Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b704ee6

Browse files
committedSep 13, 2024·
fix: create new template version when tfvars change
1 parent ee9ac22 commit b704ee6

File tree

3 files changed

+319
-77
lines changed

3 files changed

+319
-77
lines changed
 

‎docs/resources/template.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ page_title: "coderd_template Resource - terraform-provider-coderd"
44
subcategory: ""
55
description: |-
66
A Coder template.
7-
Logs from building template versions are streamed from the provisioner when the TF_LOG environment variable is INFO or higher.
7+
Logs from building template versions can be optionally streamed from the provisioner by setting the TF_LOG environment variable to INFO or higher.
88
When importing, the ID supplied can be either a template UUID retrieved via the API or <organization-name>/<template-name>.
99
---
1010

1111
# coderd_template (Resource)
1212

1313
A Coder template.
1414

15-
Logs from building template versions are streamed from the provisioner when the `TF_LOG` environment variable is `INFO` or higher.
15+
Logs from building template versions can be optionally streamed from the provisioner by setting the `TF_LOG` environment variable to `INFO` or higher.
1616

1717
When importing, the ID supplied can be either a template UUID retrieved via the API or `<organization-name>/<template-name>`.
1818

@@ -101,7 +101,7 @@ Optional:
101101

102102
- `active` (Boolean) Whether this version is the active version of the template. Only one version can be active at a time.
103103
- `message` (String) A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated.
104-
- `name` (String) The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents are updated.
104+
- `name` (String) The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents, or the `tf_vars` attribute are updated.
105105
- `provisioner_tags` (Attributes Set) Provisioner tags for the template version. (see [below for nested schema](#nestedatt--versions--provisioner_tags))
106106
- `tf_vars` (Attributes Set) Terraform variables for the template version. (see [below for nested schema](#nestedatt--versions--tf_vars))
107107

‎internal/provider/template_resource.go

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9+
"slices"
910
"strings"
1011

1112
"cdr.dev/slog"
@@ -230,8 +231,8 @@ func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRe
230231

231232
func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
232233
resp.Schema = schema.Schema{
233-
MarkdownDescription: "A Coder template.\n\nLogs from building template versions are streamed from the provisioner " +
234-
"when the `TF_LOG` environment variable is `INFO` or higher.\n\n" +
234+
MarkdownDescription: "A Coder template.\n\nLogs from building template versions can be optionally streamed from the provisioner " +
235+
"by setting the `TF_LOG` environment variable to `INFO` or higher.\n\n" +
235236
"When importing, the ID supplied can be either a template UUID retrieved via the API or `<organization-name>/<template-name>`.",
236237

237238
Attributes: map[string]schema.Attribute{
@@ -395,7 +396,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
395396
Computed: true,
396397
},
397398
"name": schema.StringAttribute{
398-
MarkdownDescription: "The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents are updated.",
399+
MarkdownDescription: "The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents, or the `tf_vars` attribute are updated.",
399400
Optional: true,
400401
Computed: true,
401402
Validators: []validator.String{
@@ -1053,7 +1054,7 @@ func markActive(ctx context.Context, client *codersdk.Client, templateID uuid.UU
10531054
ID: versionID,
10541055
})
10551056
if err != nil {
1056-
return fmt.Errorf("Failed to update active template version: %s", err)
1057+
return fmt.Errorf("failed to update active template version: %s", err)
10571058
}
10581059
tflog.Info(ctx, "marked template version as active")
10591060
return nil
@@ -1231,8 +1232,9 @@ type LastVersionsByHash = map[string][]PreviousTemplateVersion
12311232
var LastVersionsKey = "last_versions"
12321233

12331234
type PreviousTemplateVersion struct {
1234-
ID uuid.UUID `json:"id"`
1235-
Name string `json:"name"`
1235+
ID uuid.UUID `json:"id"`
1236+
Name string `json:"name"`
1237+
TFVars map[string]string `json:"tf_vars"`
12361238
}
12371239

12381240
type privateState interface {
@@ -1244,18 +1246,24 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
12441246
lv := make(LastVersionsByHash)
12451247
for _, version := range v {
12461248
vbh, ok := lv[version.DirectoryHash.ValueString()]
1249+
tfVars := make(map[string]string, len(version.TerraformVariables))
1250+
for _, tfVar := range version.TerraformVariables {
1251+
tfVars[tfVar.Name.ValueString()] = tfVar.Value.ValueString()
1252+
}
12471253
// Store the IDs and names of all versions with the same directory hash,
12481254
// in the order they appear
12491255
if ok {
12501256
lv[version.DirectoryHash.ValueString()] = append(vbh, PreviousTemplateVersion{
1251-
ID: version.ID.ValueUUID(),
1252-
Name: version.Name.ValueString(),
1257+
ID: version.ID.ValueUUID(),
1258+
Name: version.Name.ValueString(),
1259+
TFVars: tfVars,
12531260
})
12541261
} else {
12551262
lv[version.DirectoryHash.ValueString()] = []PreviousTemplateVersion{
12561263
{
1257-
ID: version.ID.ValueUUID(),
1258-
Name: version.Name.ValueString(),
1264+
ID: version.ID.ValueUUID(),
1265+
Name: version.Name.ValueString(),
1266+
TFVars: tfVars,
12591267
},
12601268
}
12611269
}
@@ -1269,6 +1277,13 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
12691277
}
12701278

12711279
func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVersions Versions) {
1280+
// We remove versions that we've matched from `lv`, so make a copy for
1281+
// resolving tfvar changes at the end.
1282+
fullLv := make(LastVersionsByHash)
1283+
for k, v := range lv {
1284+
fullLv[k] = slices.Clone(v)
1285+
}
1286+
12721287
for i := range planVersions {
12731288
prevList, ok := lv[planVersions[i].DirectoryHash.ValueString()]
12741289
// If not in state, mark as known after apply since we'll create a new version.
@@ -1308,4 +1323,47 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe
13081323
lv[planVersions[i].DirectoryHash.ValueString()] = prevList[1:]
13091324
}
13101325
}
1326+
1327+
// If only the Terraform variables have changed,
1328+
// we need to create a new version with the new variables.
1329+
for i := range planVersions {
1330+
if !planVersions[i].ID.IsUnknown() {
1331+
prevs, ok := fullLv[planVersions[i].DirectoryHash.ValueString()]
1332+
if !ok {
1333+
continue
1334+
}
1335+
if tfVariablesChanged(prevs, &planVersions[i]) {
1336+
planVersions[i].ID = NewUUIDUnknown()
1337+
// We could always set the name to unknown here, to generate a
1338+
// random one (this is what the Web UI currently does when
1339+
// only updating tfvars).
1340+
// However, I think it'd be weird if the provider just started
1341+
// ignoring the name you set in the config, we'll instead
1342+
// require that users update the name if they update the tfvars.
1343+
if configVersions[i].Name.IsNull() {
1344+
planVersions[i].Name = types.StringUnknown()
1345+
}
1346+
}
1347+
}
1348+
}
1349+
}
1350+
1351+
func tfVariablesChanged(prevs []PreviousTemplateVersion, planned *TemplateVersion) bool {
1352+
for _, prev := range prevs {
1353+
if prev.ID == planned.ID.ValueUUID() {
1354+
// If the previous version has no TFVars, then it was created using
1355+
// an older provider version.
1356+
if prev.TFVars == nil {
1357+
return true
1358+
}
1359+
for _, tfVar := range planned.TerraformVariables {
1360+
if prev.TFVars[tfVar.Name.ValueString()] != tfVar.Value.ValueString() {
1361+
return true
1362+
}
1363+
}
1364+
return false
1365+
}
1366+
}
1367+
return true
1368+
13111369
}

‎internal/provider/template_resource_test.go

Lines changed: 248 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,15 @@ func TestAccTemplateResource(t *testing.T) {
286286
cfg5.Versions = slices.Clone(cfg5.Versions)
287287
cfg5.Versions[1].Directory = PtrTo("../../integration/template-test/example-template/")
288288

289+
cfg6 := cfg5
290+
cfg6.Versions = slices.Clone(cfg6.Versions)
291+
cfg6.Versions[0].TerraformVariables = []testAccTemplateKeyValueConfig{
292+
{
293+
Key: PtrTo("name"),
294+
Value: PtrTo("world2"),
295+
},
296+
}
297+
289298
resource.Test(t, resource.TestCase{
290299
PreCheck: func() { testAccPreCheck(t) },
291300
IsUnitTest: true,
@@ -343,6 +352,66 @@ func TestAccTemplateResource(t *testing.T) {
343352
testAccCheckNumTemplateVersions(ctx, client, 4),
344353
),
345354
},
355+
// Update the Terraform variables of the first version
356+
{
357+
Config: cfg6.String(t),
358+
Check: resource.ComposeAggregateTestCheckFunc(
359+
testAccCheckNumTemplateVersions(ctx, client, 5),
360+
),
361+
},
362+
},
363+
})
364+
})
365+
366+
t.Run("AutoGenNameUpdateTFVars", func(t *testing.T) {
367+
cfg1 := testAccTemplateResourceConfig{
368+
URL: client.URL.String(),
369+
Token: client.SessionToken(),
370+
Name: PtrTo("example-template3"),
371+
Versions: []testAccTemplateVersionConfig{
372+
{
373+
// Auto-generated version name
374+
Directory: PtrTo("../../integration/template-test/example-template-2/"),
375+
TerraformVariables: []testAccTemplateKeyValueConfig{
376+
{
377+
Key: PtrTo("name"),
378+
Value: PtrTo("world"),
379+
},
380+
},
381+
Active: PtrTo(true),
382+
},
383+
},
384+
ACL: testAccTemplateACLConfig{
385+
null: true,
386+
},
387+
}
388+
389+
cfg2 := cfg1
390+
cfg2.Versions = slices.Clone(cfg2.Versions)
391+
cfg2.Versions[0].TerraformVariables = []testAccTemplateKeyValueConfig{
392+
{
393+
Key: PtrTo("name"),
394+
Value: PtrTo("world2"),
395+
},
396+
}
397+
398+
resource.Test(t, resource.TestCase{
399+
PreCheck: func() { testAccPreCheck(t) },
400+
IsUnitTest: true,
401+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
402+
Steps: []resource.TestStep{
403+
{
404+
Config: cfg1.String(t),
405+
Check: resource.ComposeAggregateTestCheckFunc(
406+
testAccCheckNumTemplateVersions(ctx, client, 1),
407+
),
408+
},
409+
{
410+
Config: cfg2.String(t),
411+
Check: resource.ComposeAggregateTestCheckFunc(
412+
testAccCheckNumTemplateVersions(ctx, client, 2),
413+
),
414+
},
346415
},
347416
})
348417
})
@@ -779,14 +848,16 @@ func TestReconcileVersionIDs(t *testing.T) {
779848
Name: "IdenticalDontRename",
780849
planVersions: []TemplateVersion{
781850
{
782-
Name: types.StringValue("foo"),
783-
DirectoryHash: types.StringValue("aaa"),
784-
ID: NewUUIDUnknown(),
851+
Name: types.StringValue("foo"),
852+
DirectoryHash: types.StringValue("aaa"),
853+
ID: NewUUIDUnknown(),
854+
TerraformVariables: []Variable{},
785855
},
786856
{
787-
Name: types.StringValue("bar"),
788-
DirectoryHash: types.StringValue("aaa"),
789-
ID: NewUUIDUnknown(),
857+
Name: types.StringValue("bar"),
858+
DirectoryHash: types.StringValue("aaa"),
859+
ID: NewUUIDUnknown(),
860+
TerraformVariables: []Variable{},
790861
},
791862
},
792863
configVersions: []TemplateVersion{
@@ -800,36 +871,41 @@ func TestReconcileVersionIDs(t *testing.T) {
800871
inputState: map[string][]PreviousTemplateVersion{
801872
"aaa": {
802873
{
803-
ID: aUUID,
804-
Name: "bar",
874+
ID: aUUID,
875+
Name: "bar",
876+
TFVars: map[string]string{},
805877
},
806878
},
807879
},
808880
expectedVersions: []TemplateVersion{
809881
{
810-
Name: types.StringValue("foo"),
811-
DirectoryHash: types.StringValue("aaa"),
812-
ID: NewUUIDUnknown(),
882+
Name: types.StringValue("foo"),
883+
DirectoryHash: types.StringValue("aaa"),
884+
ID: NewUUIDUnknown(),
885+
TerraformVariables: []Variable{},
813886
},
814887
{
815-
Name: types.StringValue("bar"),
816-
DirectoryHash: types.StringValue("aaa"),
817-
ID: UUIDValue(aUUID),
888+
Name: types.StringValue("bar"),
889+
DirectoryHash: types.StringValue("aaa"),
890+
ID: UUIDValue(aUUID),
891+
TerraformVariables: []Variable{},
818892
},
819893
},
820894
},
821895
{
822896
Name: "IdenticalRenameFirst",
823897
planVersions: []TemplateVersion{
824898
{
825-
Name: types.StringValue("foo"),
826-
DirectoryHash: types.StringValue("aaa"),
827-
ID: NewUUIDUnknown(),
899+
Name: types.StringValue("foo"),
900+
DirectoryHash: types.StringValue("aaa"),
901+
ID: NewUUIDUnknown(),
902+
TerraformVariables: []Variable{},
828903
},
829904
{
830-
Name: types.StringValue("bar"),
831-
DirectoryHash: types.StringValue("aaa"),
832-
ID: NewUUIDUnknown(),
905+
Name: types.StringValue("bar"),
906+
DirectoryHash: types.StringValue("aaa"),
907+
ID: NewUUIDUnknown(),
908+
TerraformVariables: []Variable{},
833909
},
834910
},
835911
configVersions: []TemplateVersion{
@@ -843,36 +919,41 @@ func TestReconcileVersionIDs(t *testing.T) {
843919
inputState: map[string][]PreviousTemplateVersion{
844920
"aaa": {
845921
{
846-
ID: aUUID,
847-
Name: "baz",
922+
ID: aUUID,
923+
Name: "baz",
924+
TFVars: map[string]string{},
848925
},
849926
},
850927
},
851928
expectedVersions: []TemplateVersion{
852929
{
853-
Name: types.StringValue("foo"),
854-
DirectoryHash: types.StringValue("aaa"),
855-
ID: UUIDValue(aUUID),
930+
Name: types.StringValue("foo"),
931+
DirectoryHash: types.StringValue("aaa"),
932+
ID: UUIDValue(aUUID),
933+
TerraformVariables: []Variable{},
856934
},
857935
{
858-
Name: types.StringValue("bar"),
859-
DirectoryHash: types.StringValue("aaa"),
860-
ID: NewUUIDUnknown(),
936+
Name: types.StringValue("bar"),
937+
DirectoryHash: types.StringValue("aaa"),
938+
ID: NewUUIDUnknown(),
939+
TerraformVariables: []Variable{},
861940
},
862941
},
863942
},
864943
{
865944
Name: "IdenticalHashesInState",
866945
planVersions: []TemplateVersion{
867946
{
868-
Name: types.StringValue("foo"),
869-
DirectoryHash: types.StringValue("aaa"),
870-
ID: NewUUIDUnknown(),
947+
Name: types.StringValue("foo"),
948+
DirectoryHash: types.StringValue("aaa"),
949+
ID: NewUUIDUnknown(),
950+
TerraformVariables: []Variable{},
871951
},
872952
{
873-
Name: types.StringValue("bar"),
874-
DirectoryHash: types.StringValue("aaa"),
875-
ID: NewUUIDUnknown(),
953+
Name: types.StringValue("bar"),
954+
DirectoryHash: types.StringValue("aaa"),
955+
ID: NewUUIDUnknown(),
956+
TerraformVariables: []Variable{},
876957
},
877958
},
878959
configVersions: []TemplateVersion{
@@ -886,40 +967,46 @@ func TestReconcileVersionIDs(t *testing.T) {
886967
inputState: map[string][]PreviousTemplateVersion{
887968
"aaa": {
888969
{
889-
ID: aUUID,
890-
Name: "qux",
970+
ID: aUUID,
971+
Name: "qux",
972+
TFVars: map[string]string{},
891973
},
892974
{
893-
ID: bUUID,
894-
Name: "baz",
975+
ID: bUUID,
976+
Name: "baz",
977+
TFVars: map[string]string{},
895978
},
896979
},
897980
},
898981
expectedVersions: []TemplateVersion{
899982
{
900-
Name: types.StringValue("foo"),
901-
DirectoryHash: types.StringValue("aaa"),
902-
ID: UUIDValue(aUUID),
983+
Name: types.StringValue("foo"),
984+
DirectoryHash: types.StringValue("aaa"),
985+
ID: UUIDValue(aUUID),
986+
TerraformVariables: []Variable{},
903987
},
904988
{
905-
Name: types.StringValue("bar"),
906-
DirectoryHash: types.StringValue("aaa"),
907-
ID: UUIDValue(bUUID),
989+
Name: types.StringValue("bar"),
990+
DirectoryHash: types.StringValue("aaa"),
991+
ID: UUIDValue(bUUID),
992+
TerraformVariables: []Variable{},
908993
},
909994
},
910995
},
911996
{
912997
Name: "UnknownUsesStateInOrder",
913998
planVersions: []TemplateVersion{
914999
{
915-
Name: types.StringValue("foo"),
916-
DirectoryHash: types.StringValue("aaa"),
917-
ID: NewUUIDUnknown(),
1000+
Name: types.StringValue("foo"),
1001+
DirectoryHash: types.StringValue("aaa"),
1002+
ID: NewUUIDUnknown(),
1003+
TerraformVariables: []Variable{},
9181004
},
9191005
{
920-
Name: types.StringUnknown(),
921-
DirectoryHash: types.StringValue("aaa"),
922-
ID: NewUUIDUnknown(),
1006+
Name: types.StringUnknown(),
1007+
DirectoryHash: types.StringValue("aaa"),
1008+
ID: NewUUIDUnknown(),
1009+
TerraformVariables: []Variable{},
9231010
},
9241011
},
9251012
configVersions: []TemplateVersion{
@@ -933,55 +1020,152 @@ func TestReconcileVersionIDs(t *testing.T) {
9331020
inputState: map[string][]PreviousTemplateVersion{
9341021
"aaa": {
9351022
{
936-
ID: aUUID,
937-
Name: "qux",
1023+
ID: aUUID,
1024+
Name: "qux",
1025+
TFVars: map[string]string{},
9381026
},
9391027
{
940-
ID: bUUID,
941-
Name: "baz",
1028+
ID: bUUID,
1029+
Name: "baz",
1030+
TFVars: map[string]string{},
9421031
},
9431032
},
9441033
},
9451034
expectedVersions: []TemplateVersion{
1035+
{
1036+
Name: types.StringValue("foo"),
1037+
DirectoryHash: types.StringValue("aaa"),
1038+
ID: UUIDValue(aUUID),
1039+
TerraformVariables: []Variable{},
1040+
},
1041+
{
1042+
Name: types.StringValue("baz"),
1043+
DirectoryHash: types.StringValue("aaa"),
1044+
ID: UUIDValue(bUUID),
1045+
TerraformVariables: []Variable{},
1046+
},
1047+
},
1048+
},
1049+
{
1050+
Name: "NewVersionNewRandomName",
1051+
planVersions: []TemplateVersion{
1052+
{
1053+
Name: types.StringValue("weird_draught12"),
1054+
DirectoryHash: types.StringValue("bbb"),
1055+
ID: UUIDValue(aUUID),
1056+
TerraformVariables: []Variable{},
1057+
},
1058+
},
1059+
configVersions: []TemplateVersion{
1060+
{
1061+
Name: types.StringNull(),
1062+
},
1063+
},
1064+
inputState: map[string][]PreviousTemplateVersion{
1065+
"aaa": {
1066+
{
1067+
ID: aUUID,
1068+
Name: "weird_draught12",
1069+
TFVars: map[string]string{},
1070+
},
1071+
},
1072+
},
1073+
expectedVersions: []TemplateVersion{
1074+
{
1075+
Name: types.StringUnknown(),
1076+
DirectoryHash: types.StringValue("bbb"),
1077+
ID: NewUUIDUnknown(),
1078+
TerraformVariables: []Variable{},
1079+
},
1080+
},
1081+
},
1082+
{
1083+
Name: "IdenticalNewVars",
1084+
planVersions: []TemplateVersion{
9461085
{
9471086
Name: types.StringValue("foo"),
9481087
DirectoryHash: types.StringValue("aaa"),
9491088
ID: UUIDValue(aUUID),
1089+
TerraformVariables: []Variable{
1090+
{
1091+
Name: types.StringValue("foo"),
1092+
Value: types.StringValue("bar"),
1093+
},
1094+
},
9501095
},
1096+
},
1097+
configVersions: []TemplateVersion{
9511098
{
952-
Name: types.StringValue("baz"),
1099+
Name: types.StringValue("foo"),
1100+
},
1101+
},
1102+
inputState: map[string][]PreviousTemplateVersion{
1103+
"aaa": {
1104+
{
1105+
ID: aUUID,
1106+
Name: "foo",
1107+
TFVars: map[string]string{
1108+
"foo": "foo",
1109+
},
1110+
},
1111+
},
1112+
},
1113+
expectedVersions: []TemplateVersion{
1114+
{
1115+
Name: types.StringValue("foo"),
9531116
DirectoryHash: types.StringValue("aaa"),
954-
ID: UUIDValue(bUUID),
1117+
ID: NewUUIDUnknown(),
1118+
TerraformVariables: []Variable{
1119+
{
1120+
Name: types.StringValue("foo"),
1121+
Value: types.StringValue("bar"),
1122+
},
1123+
},
9551124
},
9561125
},
9571126
},
9581127
{
959-
Name: "NewVersionNewRandomName",
1128+
Name: "IdenticalSameVars",
9601129
planVersions: []TemplateVersion{
9611130
{
962-
Name: types.StringValue("weird_draught12"),
963-
DirectoryHash: types.StringValue("bbb"),
1131+
Name: types.StringValue("foo"),
1132+
DirectoryHash: types.StringValue("aaa"),
9641133
ID: UUIDValue(aUUID),
1134+
TerraformVariables: []Variable{
1135+
{
1136+
Name: types.StringValue("foo"),
1137+
Value: types.StringValue("bar"),
1138+
},
1139+
},
9651140
},
9661141
},
9671142
configVersions: []TemplateVersion{
9681143
{
969-
Name: types.StringNull(),
1144+
Name: types.StringValue("foo"),
9701145
},
9711146
},
9721147
inputState: map[string][]PreviousTemplateVersion{
9731148
"aaa": {
9741149
{
9751150
ID: aUUID,
976-
Name: "weird_draught12",
1151+
Name: "foo",
1152+
TFVars: map[string]string{
1153+
"foo": "bar",
1154+
},
9771155
},
9781156
},
9791157
},
9801158
expectedVersions: []TemplateVersion{
9811159
{
982-
Name: types.StringUnknown(),
983-
DirectoryHash: types.StringValue("bbb"),
984-
ID: NewUUIDUnknown(),
1160+
Name: types.StringValue("foo"),
1161+
DirectoryHash: types.StringValue("aaa"),
1162+
ID: UUIDValue(aUUID),
1163+
TerraformVariables: []Variable{
1164+
{
1165+
Name: types.StringValue("foo"),
1166+
Value: types.StringValue("bar"),
1167+
},
1168+
},
9851169
},
9861170
},
9871171
},

0 commit comments

Comments
 (0)
Please sign in to comment.