Skip to content

preserve keys order for original_yaml_string #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
terraform-provider-codefresh
dist/
.vscode/

**/.terraform
**/terraform.tfstate
Expand Down
2 changes: 1 addition & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor)
WEBSITE_REPO=github.com/hashicorp/terraform-website
HOSTNAME=codefresh.io
PKG_NAME=codefresh
NAMESPACE=codefresh-io
NAMESPACE=app
BINARY=terraform-provider-${PKG_NAME}
VERSION=0.1.0
OS_ARCH=darwin_amd64
Expand Down
92 changes: 19 additions & 73 deletions codefresh/resource_pipeline.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package codefresh

import (
"encoding/json"
"fmt"
"log"
"regexp"
"strings"

cfClient "github.com/codefresh-io/terraform-provider-codefresh/client"
ghodss "github.com/ghodss/yaml"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -701,92 +701,41 @@ func mapResourceToPipeline(d *schema.ResourceData) *cfClient.Pipeline {
// We cannot leverage on the standard marshal/unmarshal because the steps attribute needs to maintain the order of elements
// while by default the standard function doesn't do it because in JSON maps are unordered
func extractSpecAttributesFromOriginalYamlString(originalYamlString string, pipeline *cfClient.Pipeline) {
// Use mapSlice to preserve order of items from the YAML string
m := yaml.MapSlice{}
err := yaml.Unmarshal([]byte(originalYamlString), &m)
ms := OrderedMapSlice{}
err := yaml.Unmarshal([]byte(originalYamlString), &ms)
if err != nil {
log.Fatalf("Unable to unmarshall original_yaml_string. Error: %v", err)
}

stages := "[]"
// Dynamically build JSON object for steps using String builder
stepsBuilder := strings.Builder{}
stepsBuilder.WriteString("{")
// Dynamically build JSON object for steps using String builder
hooksBuilder := strings.Builder{}
hooksBuilder.WriteString("{")

// Parse elements of the YAML string to extract Steps and Stages if defined
for _, item := range m {
steps := "{}"
hooks := "{}"

// Parse elements of the YAML string to extract Steps, Hooks and Stages if defined
for _, item := range ms {
key := item.Key.(string)
switch key {
case "steps":
switch x := item.Value.(type) {
default:
log.Fatalf("unsupported value type: %T", item.Value)

case yaml.MapSlice:
numberOfSteps := len(x)
for index, item := range x {
// We only need to preserve order at the first level to guarantee order of the steps, hence the child nodes can be marshalled
// with the standard library
y, _ := yaml.Marshal(item.Value)
j2, _ := ghodss.YAMLToJSON(y)
stepsBuilder.WriteString("\"" + item.Key.(string) + "\" : " + string(j2))
if index < numberOfSteps-1 {
stepsBuilder.WriteString(",")
}
}
case OrderedMapSlice:
s, _ := json.Marshal(x)
steps = string(s)
}
case "stages":
// For Stages we don't have ordering issue because it's a list
y, _ := yaml.Marshal(item.Value)
j2, _ := ghodss.YAMLToJSON(y)
stages = string(j2)
s, _ := json.Marshal(item.Value)
stages = string(s)

case "hooks":
switch hooks := item.Value.(type) {
switch x := item.Value.(type) {
default:
log.Fatalf("unsupported value type: %T", item.Value)

case yaml.MapSlice:
numberOfHooks := len(hooks)
for indexHook, hook := range hooks {
// E.g. on_finish
hooksBuilder.WriteString("\"" + hook.Key.(string) + "\" : {")
numberOfAttributes := len(hook.Value.(yaml.MapSlice))
for indexAttribute, hookAttribute := range hook.Value.(yaml.MapSlice) {
attribute := hookAttribute.Key.(string)
switch attribute {
case "steps":
hooksBuilder.WriteString("\"steps\" : {")
numberOfSteps := len(hookAttribute.Value.(yaml.MapSlice))
for indexStep, step := range hookAttribute.Value.(yaml.MapSlice) {
// We only need to preserve order at the first level to guarantee order of the steps, hence the child nodes can be marshalled
// with the standard library
y, _ := yaml.Marshal(step.Value)
j2, _ := ghodss.YAMLToJSON(y)
hooksBuilder.WriteString("\"" + step.Key.(string) + "\" : " + string(j2))
if indexStep < numberOfSteps-1 {
hooksBuilder.WriteString(",")
}
}
hooksBuilder.WriteString("}")
default:
// For Other elements we don't need to preserve order
y, _ := yaml.Marshal(hookAttribute.Value)
j2, _ := ghodss.YAMLToJSON(y)
hooksBuilder.WriteString("\"" + hookAttribute.Key.(string) + "\" : " + string(j2))
}

if indexAttribute < numberOfAttributes-1 {
hooksBuilder.WriteString(",")
}
}
hooksBuilder.WriteString("}")
if indexHook < numberOfHooks-1 {
hooksBuilder.WriteString(",")
}
}
case OrderedMapSlice:
h, _ := json.Marshal(x)
hooks = string(h)
}
case "mode":
pipeline.Spec.Mode = item.Value.(string)
Expand All @@ -799,10 +748,7 @@ func extractSpecAttributesFromOriginalYamlString(originalYamlString string, pipe
log.Printf("Unsupported entry %s", key)
}
}
stepsBuilder.WriteString("}")
hooksBuilder.WriteString("}")
steps := stepsBuilder.String()
hooks := hooksBuilder.String()

pipeline.Spec.Steps = &cfClient.Steps{
Steps: steps,
}
Expand Down
39 changes: 39 additions & 0 deletions codefresh/utils_encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package codefresh

import (
"bytes"
"encoding/json"
"fmt"

"gopkg.in/yaml.v2"
)

// can be used instead the yaml.MapSlice (as an argument to yaml.Unmarshal) in order to preserve the keys order when converting to JSON later
//
// // Usage example:
// ms := OrderedMapSlice{}
// yaml.Unmarshal([]byte(originalYamlString), &ms)
// orderedJson, _ := json.Marshal(ms)
//
// implements json.Marshaler interface
type OrderedMapSlice []yaml.MapItem

func (ms OrderedMapSlice) MarshalJSON() ([]byte, error) {
// keep the order of keys while converting to json with json.Marshal(ms)

buf := &bytes.Buffer{}
buf.Write([]byte{'{'})
for i, mi := range ms {
b, err := json.Marshal(&mi.Value)
if err != nil {
return nil, err
}
buf.WriteString(fmt.Sprintf("%q:", fmt.Sprintf("%v", mi.Key)))
buf.Write(b)
if i < len(ms)-1 {
buf.Write([]byte{','})
}
}
buf.Write([]byte{'}'})
return buf.Bytes(), nil
}
102 changes: 102 additions & 0 deletions examples/pipelines/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
provider "codefresh" {
api_url = var.api_url
token = var.token
}

resource "codefresh_project" "test" {
name = "myproject"

tags = [
"docker",
]
}

resource "codefresh_pipeline" "test" {
name = "${codefresh_project.test.name}/react-sample-app"

tags = [
"production",
"docker",
]

original_yaml_string = <<EOT
version: "1.0"
hooks:
on_finish:
steps:
b:
image: alpine:3.9
commands:
- echo "echo cleanup step"
a:
image: cloudposse/slack-notifier
commands:
- echo "Notify slack"
steps:
freestyle:
image: alpine
commands:
- sleep 10
a_freestyle:
image: alpine
commands:
- sleep 10
- echo Hey!
arguments:
c: 3
a: 1
b: 2
EOT

spec {
concurrency = 1
priority = 5

# spec_template {
# repo = "codefresh-contrib/react-sample-app"
# path = "./codefresh.yml"
# revision = "master"
# context = "git"
# }

contexts = [
"context1-name",
"context2-name",
]

trigger {
branch_regex = "/.*/gi"
context = "git"
description = "Trigger for commits"
disabled = false
events = [
"push.heads"
]
modified_files_glob = ""
name = "commits"
provider = "github"
repo = "codefresh-contrib/react-sample-app"
type = "git"
}

trigger {
branch_regex = "/.*/gi"
context = "git"
description = "Trigger for tags"
disabled = false
events = [
"push.tags"
]
modified_files_glob = ""
name = "tags"
provider = "github"
repo = "codefresh-contrib/react-sample-app"
type = "git"
}

variables = {
MY_PIP_VAR = "value"
ANOTHER_PIP_VAR = "another_value"
}
}
}
2 changes: 2 additions & 0 deletions examples/pipelines/terraform.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
api_url = "http://g.codefresh.io/api"
token = ""
8 changes: 8 additions & 0 deletions examples/pipelines/vars.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
variable api_url {
type = string
}

variable token {
type = string
default = ""
}
5 changes: 3 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package main

import (
"context"
"github.com/codefresh-io/terraform-provider-codefresh/codefresh"
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
"log"
"os"

"github.com/codefresh-io/terraform-provider-codefresh/codefresh"
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
//"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

Expand Down