Skip to content

Commit 566e3de

Browse files
committed
fix!: enforce agent names be case-insensitive-unique per-workspace
1 parent 2a248b1 commit 566e3de

File tree

4 files changed

+67
-4
lines changed

4 files changed

+67
-4
lines changed

coderd/provisionerdserver/provisionerdserver.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -1891,10 +1891,12 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
18911891
appSlugs = make(map[string]struct{})
18921892
)
18931893
for _, prAgent := range protoResource.Agents {
1894-
if _, ok := agentNames[prAgent.Name]; ok {
1894+
// Agent names must be case-insensitive-unique, to be unambiguous in
1895+
// `coder_app`s and CoderVPN DNS names.
1896+
if _, ok := agentNames[strings.ToLower(prAgent.Name)]; ok {
18951897
return xerrors.Errorf("duplicate agent name %q", prAgent.Name)
18961898
}
1897-
agentNames[prAgent.Name] = struct{}{}
1899+
agentNames[strings.ToLower(prAgent.Name)] = struct{}{}
18981900

18991901
var instanceID sql.NullString
19001902
if prAgent.GetInstanceId() != "" {

coderd/provisionerdserver/provisionerdserver_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -1905,6 +1905,32 @@ func TestInsertWorkspaceResource(t *testing.T) {
19051905
})
19061906
require.ErrorContains(t, err, "duplicate app slug")
19071907
})
1908+
t.Run("DuplicateAgentNames", func(t *testing.T) {
1909+
t.Parallel()
1910+
db := dbmem.New()
1911+
job := uuid.New()
1912+
// case-insensitive-unique
1913+
err := insert(db, job, &sdkproto.Resource{
1914+
Name: "something",
1915+
Type: "aws_instance",
1916+
Agents: []*sdkproto.Agent{{
1917+
Name: "dev",
1918+
}, {
1919+
Name: "Dev",
1920+
}},
1921+
})
1922+
require.ErrorContains(t, err, "duplicate agent name")
1923+
err = insert(db, job, &sdkproto.Resource{
1924+
Name: "something",
1925+
Type: "aws_instance",
1926+
Agents: []*sdkproto.Agent{{
1927+
Name: "dev",
1928+
}, {
1929+
Name: "dev",
1930+
}},
1931+
})
1932+
require.ErrorContains(t, err, "duplicate agent name")
1933+
})
19081934
t.Run("Success", func(t *testing.T) {
19091935
t.Parallel()
19101936
db := dbmem.New()

provisioner/terraform/resources.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,12 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
215215
return nil, xerrors.Errorf("decode agent attributes: %w", err)
216216
}
217217

218-
if _, ok := agentNames[tfResource.Name]; ok {
218+
// Agent names must be case-insensitive-unique, to be unambiguous in
219+
// `coder_app`s and CoderVPN DNS names.
220+
if _, ok := agentNames[strings.ToLower(tfResource.Name)]; ok {
219221
return nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name)
220222
}
221-
agentNames[tfResource.Name] = struct{}{}
223+
agentNames[strings.ToLower(tfResource.Name)] = struct{}{}
222224

223225
// Handling for deprecated attributes. login_before_ready was replaced
224226
// by startup_script_behavior, but we still need to support it for

provisioner/terraform/resources_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,39 @@ func TestAppSlugValidation(t *testing.T) {
10261026
require.ErrorContains(t, err, "duplicate app slug")
10271027
}
10281028

1029+
func TestAgentNameDuplicate(t *testing.T) {
1030+
t.Parallel()
1031+
ctx, logger := ctxAndLogger(t)
1032+
1033+
// nolint:dogsled
1034+
_, filename, _, _ := runtime.Caller(0)
1035+
1036+
dir := filepath.Join(filepath.Dir(filename), "testdata", "multiple-agents")
1037+
tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.json"))
1038+
require.NoError(t, err)
1039+
var tfPlan tfjson.Plan
1040+
err = json.Unmarshal(tfPlanRaw, &tfPlan)
1041+
require.NoError(t, err)
1042+
tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "multiple-agents.tfplan.dot"))
1043+
require.NoError(t, err)
1044+
1045+
for _, resource := range tfPlan.PlannedValues.RootModule.Resources {
1046+
if resource.Type == "coder_agent" {
1047+
switch resource.Name {
1048+
case "dev1":
1049+
resource.Name = "dev"
1050+
case "dev2":
1051+
resource.Name = "Dev"
1052+
}
1053+
}
1054+
}
1055+
1056+
state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule}, string(tfPlanGraph), logger)
1057+
require.Nil(t, state)
1058+
require.Error(t, err)
1059+
require.ErrorContains(t, err, "duplicate agent name")
1060+
}
1061+
10291062
func TestMetadataResourceDuplicate(t *testing.T) {
10301063
t.Parallel()
10311064
ctx, logger := ctxAndLogger(t)

0 commit comments

Comments
 (0)