feat: preserve original order of rich parameters (#6747)

* WIP

* orderedParameters

* fix

* WIP

* TestS
This commit is contained in:
Marcin Tojek
2023-03-24 09:37:27 +01:00
committed by GitHub
parent 9822745365
commit c7fb5f960c
5 changed files with 137 additions and 17 deletions
+17 -2
View File
@@ -259,7 +259,17 @@ func (e *executor) planResources(ctx, killCtx context.Context, planfilePath stri
modules = append(modules, plan.PriorState.Values.RootModule)
}
modules = append(modules, plan.PlannedValues.RootModule)
return ConvertState(modules, rawGraph)
rawParameterNames, err := rawRichParameterNames(e.workdir)
if err != nil {
return nil, xerrors.Errorf("raw rich parameter names: %w", err)
}
state, err := ConvertState(modules, rawGraph, rawParameterNames)
if err != nil {
return nil, err
}
return state, nil
}
// showPlan must only be called while the lock is held.
@@ -366,9 +376,14 @@ func (e *executor) stateResources(ctx, killCtx context.Context) (*State, error)
}
converted := &State{}
if state.Values != nil {
rawParameterNames, err := rawRichParameterNames(e.workdir)
if err != nil {
return nil, xerrors.Errorf("raw rich parameter names: %w", err)
}
converted, err = ConvertState([]*tfjson.StateModule{
state.Values.RootModule,
}, rawGraph)
}, rawGraph, rawParameterNames)
if err != nil {
return nil, err
}
+55
View File
@@ -0,0 +1,55 @@
package terraform
import (
"fmt"
"os"
"path"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
)
var terraformWithCoderParametersSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "data",
LabelNames: []string{"coder_parameter", "*"},
},
},
}
func rawRichParameterNames(workdir string) ([]string, error) {
entries, err := os.ReadDir(workdir)
if err != nil {
return nil, err
}
var coderParameterNames []string
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".tf") {
continue
}
hclFilepath := path.Join(workdir, entry.Name())
parser := hclparse.NewParser()
parsedHCL, diags := parser.ParseHCLFile(hclFilepath)
if diags.HasErrors() {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Failed to parse HCL file",
Detail: fmt.Sprintf("parser.ParseHCLFile can't parse %q file", hclFilepath),
},
}
}
content, _, _ := parsedHCL.Body.PartialContent(terraformWithCoderParametersSchema)
for _, block := range content.Blocks {
if block.Type == "data" && block.Labels[0] == "coder_parameter" && len(block.Labels) == 2 {
coderParameterNames = append(coderParameterNames, block.Labels[1])
}
}
}
return coderParameterNames, nil
}
+32 -4
View File
@@ -328,11 +328,17 @@ func TestProvision(t *testing.T) {
required_providers {
coder = {
source = "coder/coder"
version = "0.6.6"
version = "0.6.20"
}
}
}
data "coder_parameter" "sample" {
name = "Sample"
type = "string"
default = "foobaz"
}
data "coder_parameter" "example" {
name = "Example"
type = "string"
@@ -347,6 +353,10 @@ func TestProvision(t *testing.T) {
},
Request: &proto.Provision_Plan{
RichParameterValues: []*proto.RichParameterValue{
{
Name: "Sample",
Value: "foofoo",
},
{
Name: "Example",
Value: "foobaz",
@@ -356,6 +366,18 @@ func TestProvision(t *testing.T) {
Response: &proto.Provision_Response{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{
Name: "Sample",
Type: "string",
DefaultValue: "foobaz",
},
{
Name: "Example",
Type: "string",
DefaultValue: "foobar",
},
},
Resources: []*proto.Resource{{
Name: "example",
Type: "null_resource",
@@ -441,6 +463,7 @@ func TestProvision(t *testing.T) {
planRequest.GetPlan().Config = &proto.Provision_Config{}
}
planRequest.GetPlan().ParameterValues = testCase.Request.ParameterValues
planRequest.GetPlan().RichParameterValues = testCase.Request.RichParameterValues
planRequest.GetPlan().GitAuthProviders = testCase.Request.GitAuthProviders
if testCase.Request.Config != nil {
planRequest.GetPlan().Config.State = testCase.Request.Config.State
@@ -499,15 +522,20 @@ func TestProvision(t *testing.T) {
}
if testCase.Response != nil {
require.Equal(t, testCase.Response.GetComplete().Error, msg.GetComplete().Error)
resourcesGot, err := json.Marshal(msg.GetComplete().Resources)
require.NoError(t, err)
resourcesWant, err := json.Marshal(testCase.Response.GetComplete().Resources)
require.NoError(t, err)
require.Equal(t, testCase.Response.GetComplete().Error, msg.GetComplete().Error)
require.Equal(t, string(resourcesWant), string(resourcesGot))
parametersGot, err := json.Marshal(msg.GetComplete().Parameters)
require.NoError(t, err)
parametersWant, err := json.Marshal(testCase.Response.GetComplete().Parameters)
require.NoError(t, err)
require.Equal(t, string(parametersWant), string(parametersGot))
}
break
}
+18 -5
View File
@@ -83,7 +83,7 @@ type State struct {
// ConvertState consumes Terraform state and a GraphViz representation
// produced by `terraform graph` to produce resources consumable by Coder.
// nolint:gocyclo
func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error) {
func ConvertState(modules []*tfjson.StateModule, rawGraph string, rawParameterNames []string) (*State, error) {
parsedGraph, err := gographviz.ParseString(rawGraph)
if err != nil {
return nil, xerrors.Errorf("parse graph: %w", err)
@@ -442,10 +442,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
}
parameters := make([]*proto.RichParameter, 0)
for _, resource := range tfResourcesRichParameters {
if resource.Type != "coder_parameter" {
continue
}
for _, resource := range orderedRichParametersResources(tfResourcesRichParameters, rawParameterNames) {
var param provider.Parameter
err = mapstructure.Decode(resource.AttributeValues, &param)
if err != nil {
@@ -629,3 +626,19 @@ func findResourcesInGraph(graph *gographviz.Graph, tfResourcesByLabel map[string
return graphResources
}
func orderedRichParametersResources(tfResourcesRichParameters []*tfjson.StateResource, orderedNames []string) []*tfjson.StateResource {
if len(orderedNames) == 0 {
return tfResourcesRichParameters
}
ordered := make([]*tfjson.StateResource, len(orderedNames))
for i, name := range orderedNames {
for _, resource := range tfResourcesRichParameters {
if resource.Name == name {
ordered[i] = resource
}
}
}
return ordered
}
+15 -6
View File
@@ -6,6 +6,7 @@ import (
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
protobuf "github.com/golang/protobuf/proto"
@@ -349,7 +350,7 @@ func TestConvertResources(t *testing.T) {
// and that no errors occur!
modules = append(modules, tfPlan.PlannedValues.RootModule)
}
state, err := terraform.ConvertState(modules, string(tfPlanGraph))
state, err := terraform.ConvertState(modules, string(tfPlanGraph), richParameterResourceNames(expected.parameters))
require.NoError(t, err)
sortResources(state.Resources)
sort.Strings(state.GitAuthProviders)
@@ -402,7 +403,7 @@ func TestConvertResources(t *testing.T) {
tfStateGraph, err := os.ReadFile(filepath.Join(dir, folderName+".tfstate.dot"))
require.NoError(t, err)
state, err := terraform.ConvertState([]*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph))
state, err := terraform.ConvertState([]*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph), richParameterResourceNames(expected.parameters))
require.NoError(t, err)
sortResources(state.Resources)
sort.Strings(state.GitAuthProviders)
@@ -461,7 +462,7 @@ func TestAppSlugValidation(t *testing.T) {
}
}
state, err := terraform.ConvertState([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph))
state, err := terraform.ConvertState([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), nil)
require.Nil(t, state)
require.Error(t, err)
require.ErrorContains(t, err, "invalid app slug")
@@ -473,7 +474,7 @@ func TestAppSlugValidation(t *testing.T) {
}
}
state, err = terraform.ConvertState([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph))
state, err = terraform.ConvertState([]*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), nil)
require.Nil(t, state)
require.Error(t, err)
require.ErrorContains(t, err, "duplicate app slug")
@@ -523,7 +524,7 @@ func TestInstanceTypeAssociation(t *testing.T) {
subgraph "root" {
"[root] `+tc.ResourceType+`.dev" [label = "`+tc.ResourceType+`.dev", shape = "box"]
}
}`)
}`, nil)
require.NoError(t, err)
require.Len(t, state.Resources, 1)
require.Equal(t, state.Resources[0].GetInstanceType(), instanceType)
@@ -594,7 +595,7 @@ func TestInstanceIDAssociation(t *testing.T) {
"[root] `+tc.ResourceType+`.dev" -> "[root] coder_agent.dev"
}
}
`)
`, nil)
require.NoError(t, err)
require.Len(t, state.Resources, 1)
require.Len(t, state.Resources[0].Agents, 1)
@@ -623,3 +624,11 @@ func sortResources(resources []*proto.Resource) {
})
}
}
func richParameterResourceNames(parameters []*proto.RichParameter) []string {
var names []string
for _, p := range parameters {
names = append(names, strings.ToLower(p.Name))
}
return names
}