Skip to content

Commit 77ee678

Browse files
authored
Fix: spec attribute propagation from original_source_yaml (#113)
## What * Fix spec attribute propagation from `original_source_yaml` * Misc: fix error handling in spec attribute propagation ## Why * When defining a Pipeline spec via original_yaml_string, it will fail to run. See #112 ## Notes <!-- Add any notes here --> Closes #112 ## Checklist * [x] _I have read [CONTRIBUTING.md](https://github.com/codefresh-io/terraform-provider-codefresh/blob/master/README.md)._ * [x] _I have [allowed changes to my fork to be made](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork)._ * [x] _I have added tests, assuming new tests are warranted_. * [x] _I understand that the `/test` comment will be ignored by the CI trigger [unless it is made by a repo admin or collaborator](https://codefresh.io/docs/docs/pipelines/triggers/git-triggers/#support-for-building-pull-requests-from-forks)._
1 parent 2133d39 commit 77ee678

File tree

7 files changed

+201
-103
lines changed

7 files changed

+201
-103
lines changed

codefresh.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ steps:
1515
go_fmt:
1616
title: "Formatting"
1717
stage: test
18-
image: goreleaser/goreleaser:v1.15.2
18+
image: goreleaser/goreleaser:v1.17.0
1919
commands:
2020
- go fmt
2121

2222
go_test:
2323
title: "Run tests"
2424
stage: test
25-
image: goreleaser/goreleaser:v1.15.2
25+
image: goreleaser/goreleaser:v1.17.0
2626
environment:
2727
- TF_ACC="test"
2828
commands:
@@ -52,7 +52,7 @@ steps:
5252

5353
release_binaries:
5454
title: Create release in Github
55-
image: goreleaser/goreleaser:v1.15.2
55+
image: goreleaser/goreleaser:v1.17.0
5656
stage: release
5757
environment:
5858
- GPG_FINGERPRINT=${{GPG_FINGERPRINT}}

codefresh/resource_pipeline.go

+59-60
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
package codefresh
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"log"
76
"regexp"
7+
"strconv"
88
"strings"
99

1010
cfClient "github.com/codefresh-io/terraform-provider-codefresh/client"
1111
"github.com/hashicorp/go-cty/cty"
1212
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1313
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1414
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
15-
"gopkg.in/yaml.v2"
1615
)
1716

1817
var terminationPolicyOnCreateBranchAttributes = []string{"branchName", "ignoreTrigger", "ignoreBranch"}
@@ -482,9 +481,12 @@ func resourcePipelineCreate(d *schema.ResourceData, meta interface{}) error {
482481

483482
client := meta.(*cfClient.Client)
484483

485-
pipeline := *mapResourceToPipeline(d)
484+
pipeline, err := mapResourceToPipeline(d)
485+
if err != nil {
486+
return err
487+
}
486488

487-
resp, err := client.CreatePipeline(&pipeline)
489+
resp, err := client.CreatePipeline(pipeline)
488490
if err != nil {
489491
return err
490492
}
@@ -522,10 +524,14 @@ func resourcePipelineUpdate(d *schema.ResourceData, meta interface{}) error {
522524

523525
client := meta.(*cfClient.Client)
524526

525-
pipeline := *mapResourceToPipeline(d)
527+
pipeline, err := mapResourceToPipeline(d)
528+
if err != nil {
529+
return err
530+
}
531+
526532
pipeline.Metadata.ID = d.Id()
527533

528-
_, err := client.UpdatePipeline(&pipeline)
534+
_, err = client.UpdatePipeline(pipeline)
529535
if err != nil {
530536
return err
531537
}
@@ -735,7 +741,7 @@ func flattenTriggers(triggers []cfClient.Trigger) []map[string]interface{} {
735741
return res
736742
}
737743

738-
func mapResourceToPipeline(d *schema.ResourceData) *cfClient.Pipeline {
744+
func mapResourceToPipeline(d *schema.ResourceData) (*cfClient.Pipeline, error) {
739745

740746
tags := d.Get("tags").(*schema.Set).List()
741747

@@ -774,7 +780,10 @@ func mapResourceToPipeline(d *schema.ResourceData) *cfClient.Pipeline {
774780
Context: d.Get("spec.0.spec_template.0.context").(string),
775781
}
776782
} else {
777-
extractSpecAttributesFromOriginalYamlString(originalYamlString, pipeline)
783+
err := extractSpecAttributesFromOriginalYamlString(originalYamlString, pipeline)
784+
if err != nil {
785+
return nil, err
786+
}
778787
}
779788

780789
if _, ok := d.GetOk("spec.0.runtime_environment"); ok {
@@ -870,71 +879,61 @@ func mapResourceToPipeline(d *schema.ResourceData) *cfClient.Pipeline {
870879

871880
pipeline.Spec.TerminationPolicy = codefreshTerminationPolicy
872881

873-
return pipeline
882+
return pipeline, nil
874883
}
875884

876-
// extractSpecAttributesFromOriginalYamlString extracts the steps and stages from the original yaml string to enable propagation in the `Spec` attribute of the pipeline
877-
// We cannot leverage on the standard marshal/unmarshal because the steps attribute needs to maintain the order of elements
878-
// while by default the standard function doesn't do it because in JSON maps are unordered
879-
func extractSpecAttributesFromOriginalYamlString(originalYamlString string, pipeline *cfClient.Pipeline) {
880-
ms := OrderedMapSlice{}
881-
err := yaml.Unmarshal([]byte(originalYamlString), &ms)
882-
if err != nil {
883-
log.Fatalf("Unable to unmarshall original_yaml_string. Error: %v", err)
884-
}
885+
// This function is used to extract the spec attributes from the original_yaml_string attribute.
886+
// Typically, unmarshalling the YAML string is problematic because the order of the attributes is not preserved.
887+
// Namely, we care a lot about the order of the steps and stages attributes.
888+
// Luckily, the yj package introduces a MapSlice type that preserves the order Map items (see utils.go).
889+
func extractSpecAttributesFromOriginalYamlString(originalYamlString string, pipeline *cfClient.Pipeline) error {
890+
for _, attribute := range []string{"stages", "steps", "hooks"} {
891+
yamlString, err := yq(fmt.Sprintf(".%s", attribute), originalYamlString)
892+
if err != nil {
893+
return fmt.Errorf("error while extracting '%s' from original YAML string: %v", attribute, err)
894+
} else if yamlString == "" {
895+
continue
896+
}
885897

886-
stages := "[]"
887-
steps := "{}"
888-
hooks := "{}"
898+
attributeJson, err := yamlToJson(yamlString)
899+
if err != nil {
900+
return fmt.Errorf("error while converting '%s' YAML to JSON: %v", attribute, err)
901+
}
889902

890-
// Parse elements of the YAML string to extract Steps, Hooks and Stages if defined
891-
for _, item := range ms {
892-
key := item.Key.(string)
893-
switch key {
903+
switch attribute {
904+
case "stages":
905+
pipeline.Spec.Stages = &cfClient.Stages{
906+
Stages: attributeJson,
907+
}
894908
case "steps":
895-
switch x := item.Value.(type) {
896-
default:
897-
log.Fatalf("unsupported value type: %T", item.Value)
898-
899-
case OrderedMapSlice:
900-
s, _ := json.Marshal(x)
901-
steps = string(s)
909+
pipeline.Spec.Steps = &cfClient.Steps{
910+
Steps: attributeJson,
902911
}
903-
case "stages":
904-
s, _ := json.Marshal(item.Value)
905-
stages = string(s)
906-
907912
case "hooks":
908-
switch x := item.Value.(type) {
909-
default:
910-
log.Fatalf("unsupported value type: %T", item.Value)
911-
912-
case OrderedMapSlice:
913-
h, _ := json.Marshal(x)
914-
hooks = string(h)
913+
pipeline.Spec.Hooks = &cfClient.Hooks{
914+
Hooks: attributeJson,
915915
}
916-
case "mode":
917-
pipeline.Spec.Mode = item.Value.(string)
918-
case "fail_fast":
919-
ff, ok := item.Value.(bool)
920-
if ok {
921-
pipeline.Spec.FailFast = &ff
922-
}
923-
default:
924-
log.Printf("Unsupported entry %s", key)
925916
}
926917
}
927918

928-
pipeline.Spec.Steps = &cfClient.Steps{
929-
Steps: steps,
930-
}
931-
pipeline.Spec.Stages = &cfClient.Stages{
932-
Stages: stages,
933-
}
934-
pipeline.Spec.Hooks = &cfClient.Hooks{
935-
Hooks: hooks,
919+
mode, err := yq(".mode", originalYamlString)
920+
if err != nil {
921+
return fmt.Errorf("error while extracting 'mode' from original YAML string: %v", err)
922+
} else if mode != "" {
923+
pipeline.Spec.Mode = mode
936924
}
937925

926+
ff, err := yq(".fail_fast", originalYamlString)
927+
if err != nil {
928+
return fmt.Errorf("error while extracting 'mode' from original YAML string: %v", err)
929+
} else if ff != "" {
930+
ff_b, err := strconv.ParseBool(strings.TrimSpace(ff))
931+
if err != nil {
932+
return fmt.Errorf("error while parsing 'fail_fast' as boolean: %v", err)
933+
}
934+
pipeline.Spec.FailFast = &ff_b
935+
}
936+
return nil
938937
}
939938

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

codefresh/resource_pipeline_test.go

+53-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,59 @@ func TestAccCodefreshPipeline_RuntimeEnvironment(t *testing.T) {
168168
})
169169
}
170170

171-
func TestAccCodefreshPipeline_OriginalYamlString(t *testing.T) {
171+
func TestAccCodefreshPipeline_OriginalYamlString_Steps(t *testing.T) {
172+
name := pipelineNamePrefix + acctest.RandString(10)
173+
resourceName := "codefresh_pipeline.test"
174+
originalYamlString := `version: 1.0
175+
steps:
176+
cc_firstStep:
177+
image: alpine
178+
commands:
179+
- echo Hello World First Step
180+
bb_secondStep:
181+
image: alpine
182+
commands:
183+
- echo Hello World Second jStep
184+
aa_secondStep:
185+
image: alpine
186+
commands:
187+
- echo Hello World Third Step`
188+
189+
expectedSpecAttributes := &cfClient.Spec{
190+
Steps: &cfClient.Steps{
191+
Steps: `{"cc_firstStep":{"image":"alpine","commands":["echo Hello World First Step"]},"bb_secondStep":{"image":"alpine","commands":["echo Hello World Second jStep"]},"aa_secondStep":{"image":"alpine","commands":["echo Hello World Third Step"]}}`,
192+
},
193+
Stages: &cfClient.Stages{
194+
Stages: `[]`,
195+
},
196+
}
197+
198+
var pipeline cfClient.Pipeline
199+
200+
resource.ParallelTest(t, resource.TestCase{
201+
PreCheck: func() { testAccPreCheck(t) },
202+
Providers: testAccProviders,
203+
CheckDestroy: testAccCheckCodefreshPipelineDestroy,
204+
Steps: []resource.TestStep{
205+
{
206+
Config: testAccCodefreshPipelineBasicConfigOriginalYamlString(name, originalYamlString),
207+
Check: resource.ComposeTestCheckFunc(
208+
209+
testAccCheckCodefreshPipelineExists(resourceName, &pipeline),
210+
resource.TestCheckResourceAttr(resourceName, "original_yaml_string", originalYamlString),
211+
testAccCheckCodefreshPipelineOriginalYamlStringAttributePropagation(resourceName, expectedSpecAttributes),
212+
),
213+
},
214+
{
215+
ResourceName: resourceName,
216+
ImportState: true,
217+
ImportStateVerify: true,
218+
},
219+
},
220+
})
221+
}
222+
223+
func TestAccCodefreshPipeline_OriginalYamlString_All(t *testing.T) {
172224
name := pipelineNamePrefix + acctest.RandString(10)
173225
resourceName := "codefresh_pipeline.test"
174226
originalYamlString := `version: 1.0

codefresh/utils.go

+45
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package codefresh
22

33
import (
4+
"bytes"
45
"fmt"
6+
"github.com/sclevine/yj/convert"
57
"log"
68
"regexp"
9+
"strings"
710

811
cfClient "github.com/codefresh-io/terraform-provider-codefresh/client"
912
"github.com/dlclark/regexp2"
1013
"github.com/ghodss/yaml"
1114
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
15+
"github.com/mikefarah/yq/v4/pkg/yqlib"
16+
logging "gopkg.in/op/go-logging.v1"
17+
"io/ioutil"
1218
)
1319

1420
func convertStringArr(ifaceArr []interface{}) []string {
@@ -116,3 +122,42 @@ func stringIsValidRe2RegExp(i interface{}, k string) (warnings []string, errors
116122

117123
return warnings, errors
118124
}
125+
126+
// Get a value from a YAML string using yq
127+
func yq(yamlString string, expression string) (string, error) {
128+
yqEncoder := yqlib.NewYamlEncoder(0, false, yqlib.NewDefaultYamlPreferences())
129+
yqDecoder := yqlib.NewYamlDecoder(yqlib.NewDefaultYamlPreferences())
130+
yqEvaluator := yqlib.NewStringEvaluator()
131+
132+
// Disable yq logging
133+
yqLogBackend := logging.AddModuleLevel(logging.NewLogBackend(ioutil.Discard, "", 0))
134+
yqlib.GetLogger().SetBackend(yqLogBackend)
135+
136+
yamlString, err := yqEvaluator.Evaluate(yamlString, expression, yqEncoder, yqDecoder)
137+
yamlString = strings.TrimSpace(yamlString)
138+
139+
if yamlString == "null" { // yq's Evaluate() returns "null" if the expression does not match anything
140+
return "", err
141+
}
142+
return yamlString, err
143+
}
144+
145+
// Convert a YAML string to JSON while preserving the order of map keys (courtesy of yj package).
146+
// If this were to use yaml.Unmarshal() and json.Marshal() instead, the order of map keys would be lost.
147+
func yamlToJson(yamlString string) (string, error) {
148+
yamlConverter := convert.YAML{}
149+
jsonConverter := convert.JSON{}
150+
151+
yamlDecoded, err := yamlConverter.Decode(strings.NewReader(yamlString))
152+
if err != nil {
153+
return "", err
154+
}
155+
156+
jsonBuffer := new(bytes.Buffer)
157+
err = jsonConverter.Encode(jsonBuffer, yamlDecoded)
158+
if err != nil {
159+
return "", err
160+
}
161+
162+
return jsonBuffer.String(), nil
163+
}

codefresh/utils_encoding.go

-39
This file was deleted.

0 commit comments

Comments
 (0)