mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: preserve original order of rich parameters (#6747)
* WIP * orderedParameters * fix * WIP * TestS
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, ¶m)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user