Skip to content

Commit 48e2cb8

Browse files
committed
fix: validate that parameter names are unique
1 parent 7ec16cf commit 48e2cb8

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed

coderd/util/slice/slice.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package slice
22

3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
38
// SameElements returns true if the 2 lists have the same elements in any
49
// order.
510
func SameElements[T comparable](a []T, b []T) bool {
@@ -67,3 +72,16 @@ func OverlapCompare[T any](a []T, b []T, equal func(a, b T) bool) bool {
6772
func New[T any](items ...T) []T {
6873
return items
6974
}
75+
76+
// JoinWithConjunction joins a slice of strings with commas except for the last
77+
// two which are joined with "and".
78+
func JoinWithConjunction(s []string) string {
79+
last := len(s) - 1
80+
if last == 0 {
81+
return s[last]
82+
}
83+
return fmt.Sprintf("%s and %s",
84+
strings.Join(s[:last], ", "),
85+
s[last],
86+
)
87+
}

coderd/util/slice/slice_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ func TestOverlap(t *testing.T) {
8383
)
8484
}
8585

86+
func TestJoinWithConjunction(t *testing.T) {
87+
t.Parallel()
88+
require.Equal(t, "foo", slice.JoinWithConjunction([]string{"foo"}))
89+
require.Equal(t, "foo and bar", slice.JoinWithConjunction([]string{"foo", "bar"}))
90+
require.Equal(t, "foo, bar and baz", slice.JoinWithConjunction([]string{"foo", "bar", "baz"}))
91+
}
92+
8693
func assertSetOverlaps[T comparable](t *testing.T, overlap bool, a []T, b []T) {
8794
t.Helper()
8895
for _, e := range a {

provisioner/terraform/resources.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package terraform
22

33
import (
4+
"fmt"
45
"strings"
56

67
"github.com/awalterschulze/gographviz"
@@ -10,6 +11,7 @@ import (
1011

1112
"github.com/coder/terraform-provider-coder/provider"
1213

14+
"github.com/coder/coder/coderd/util/slice"
1315
"github.com/coder/coder/provisioner"
1416
"github.com/coder/coder/provisionersdk/proto"
1517
)
@@ -462,6 +464,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string, rawParameterNa
462464
}
463465
}
464466

467+
var duplicatedNames []string
465468
parameters := make([]*proto.RichParameter, 0)
466469
for _, resource := range orderedRichParametersResources(tfResourcesRichParameters, rawParameterNames) {
467470
var param provider.Parameter
@@ -525,9 +528,32 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string, rawParameterNa
525528
})
526529
}
527530
}
531+
532+
// Check if this parameter duplicates an existing parameter.
533+
formattedName := fmt.Sprintf("%q", protoParam.Name)
534+
if !slice.Contains(duplicatedNames, formattedName) &&
535+
slice.ContainsCompare(parameters, protoParam, func(a, b *proto.RichParameter) bool {
536+
return a.Name == b.Name
537+
}) {
538+
duplicatedNames = append(duplicatedNames, formattedName)
539+
}
540+
528541
parameters = append(parameters, protoParam)
529542
}
530543

544+
// Enforce that parameters must be uniquely named.
545+
if len(duplicatedNames) > 0 {
546+
s := ""
547+
if len(duplicatedNames) == 1 {
548+
s = "s"
549+
}
550+
return nil, xerrors.Errorf(
551+
"coder_parameter names must be unique but %s appear%s multiple times",
552+
slice.JoinWithConjunction(duplicatedNames),
553+
s,
554+
)
555+
}
556+
531557
// A map is used to ensure we don't have duplicates!
532558
gitAuthProvidersMap := map[string]struct{}{}
533559
for _, tfResources := range tfResourcesByLabel {

provisioner/terraform/resources_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package terraform_test
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"os"
67
"path/filepath"
78
"runtime"
@@ -575,6 +576,69 @@ func TestAppSlugValidation(t *testing.T) {
575576
require.ErrorContains(t, err, "duplicate app slug")
576577
}
577578

579+
func TestParameterValidation(t *testing.T) {
580+
t.Parallel()
581+
582+
// nolint:dogsled
583+
_, filename, _, _ := runtime.Caller(0)
584+
585+
// Load the rich-parameters state file and edit it.
586+
dir := filepath.Join(filepath.Dir(filename), "testdata", "rich-parameters")
587+
tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "rich-parameters.tfplan.json"))
588+
require.NoError(t, err)
589+
var tfPlan tfjson.Plan
590+
err = json.Unmarshal(tfPlanRaw, &tfPlan)
591+
require.NoError(t, err)
592+
tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "rich-parameters.tfplan.dot"))
593+
require.NoError(t, err)
594+
595+
// Change all names to be identical.
596+
var names []string
597+
for _, resource := range tfPlan.PriorState.Values.RootModule.Resources {
598+
if resource.Type == "coder_parameter" {
599+
resource.AttributeValues["name"] = "identical"
600+
names = append(names, resource.Name)
601+
}
602+
}
603+
604+
state, err := terraform.ConvertState([]*tfjson.StateModule{tfPlan.PriorState.Values.RootModule}, string(tfPlanGraph), names)
605+
require.Nil(t, state)
606+
require.Error(t, err)
607+
require.ErrorContains(t, err, "coder_parameter names must be unique but \"identical\" appears multiple times")
608+
609+
// Make two sets of identical names.
610+
count := 0
611+
names = nil
612+
for _, resource := range tfPlan.PriorState.Values.RootModule.Resources {
613+
if resource.Type == "coder_parameter" {
614+
resource.AttributeValues["name"] = fmt.Sprintf("identical-%d", count%2)
615+
names = append(names, resource.Name)
616+
count++
617+
}
618+
}
619+
620+
state, err = terraform.ConvertState([]*tfjson.StateModule{tfPlan.PriorState.Values.RootModule}, string(tfPlanGraph), names)
621+
require.Nil(t, state)
622+
require.Error(t, err)
623+
require.ErrorContains(t, err, "coder_parameter names must be unique but \"identical-0\" and \"identical-1\" appear multiple times")
624+
625+
// Once more with three sets.
626+
count = 0
627+
names = nil
628+
for _, resource := range tfPlan.PriorState.Values.RootModule.Resources {
629+
if resource.Type == "coder_parameter" {
630+
resource.AttributeValues["name"] = fmt.Sprintf("identical-%d", count%3)
631+
names = append(names, resource.Name)
632+
count++
633+
}
634+
}
635+
636+
state, err = terraform.ConvertState([]*tfjson.StateModule{tfPlan.PriorState.Values.RootModule}, string(tfPlanGraph), names)
637+
require.Nil(t, state)
638+
require.Error(t, err)
639+
require.ErrorContains(t, err, "coder_parameter names must be unique but \"identical-0\", \"identical-1\" and \"identical-2\" appear multiple times")
640+
}
641+
578642
func TestInstanceTypeAssociation(t *testing.T) {
579643
t.Parallel()
580644
type tc struct {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy