Skip to content

Commit ebd130f

Browse files
fix: allow templated label values (#634)
`validateMetadataLabels` ensures that labels are in a certain format. This works fine in most cases, except when this is used within appsets, which allow for labels to be templated. This adds a layer of indirection allowing for templated labels to be used, but only when an appset makes use of this feature. Signed-off-by: Blake Pettersson <[email protected]>
1 parent 4cf0735 commit ebd130f

File tree

7 files changed

+151
-26
lines changed

7 files changed

+151
-26
lines changed

argocd/resource_argocd_application.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func resourceArgoCDApplication() *schema.Resource {
3030
},
3131
Schema: map[string]*schema.Schema{
3232
"metadata": metadataSchema("applications.argoproj.io"),
33-
"spec": applicationSpecSchemaV4(false),
33+
"spec": applicationSpecSchemaV4(false, false),
3434
"wait": {
3535
Type: schema.TypeBool,
3636
Description: "Upon application creation or update, wait for application health/sync status to be healthy/Synced, upon application deletion, wait for application to be removed, when set to true. Wait timeouts are controlled by Terraform Create, Update and Delete resource timeouts (all default to 5 minutes). **Note**: if ArgoCD decides not to sync an application (e.g. because the project to which the application belongs has a `sync_window` applied) then you will experience an expected timeout event if `wait = true`.",

argocd/schema_application.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func applicationSpecSchemaV0() *schema.Schema {
140140
Type: schema.TypeMap,
141141
Optional: true,
142142
Elem: &schema.Schema{Type: schema.TypeString},
143-
ValidateFunc: validateMetadataLabels,
143+
ValidateFunc: validateMetadataLabels(false),
144144
},
145145
"common_annotations": {
146146
Type: schema.TypeMap,
@@ -520,7 +520,7 @@ func applicationSpecSchemaV1() *schema.Schema {
520520
Type: schema.TypeMap,
521521
Optional: true,
522522
Elem: &schema.Schema{Type: schema.TypeString},
523-
ValidateFunc: validateMetadataLabels,
523+
ValidateFunc: validateMetadataLabels(false),
524524
},
525525
"common_annotations": {
526526
Type: schema.TypeMap,
@@ -951,7 +951,7 @@ func applicationSpecSchemaV2() *schema.Schema {
951951
Description: "List of additional labels to add to rendered manifests.",
952952
Optional: true,
953953
Elem: &schema.Schema{Type: schema.TypeString},
954-
ValidateFunc: validateMetadataLabels,
954+
ValidateFunc: validateMetadataLabels(false),
955955
},
956956
"common_annotations": {
957957
Type: schema.TypeMap,
@@ -1232,7 +1232,7 @@ func applicationSpecSchemaV3() *schema.Schema {
12321232
return applicationSpecSchemaV2()
12331233
}
12341234

1235-
func applicationSpecSchemaV4(allOptional bool) *schema.Schema {
1235+
func applicationSpecSchemaV4(allOptional, isAppSet bool) *schema.Schema {
12361236
return &schema.Schema{
12371237
Type: schema.TypeList,
12381238
MinItems: 1,
@@ -1432,7 +1432,7 @@ func applicationSpecSchemaV4(allOptional bool) *schema.Schema {
14321432
Description: "List of additional labels to add to rendered manifests.",
14331433
Optional: true,
14341434
Elem: &schema.Schema{Type: schema.TypeString},
1435-
ValidateFunc: validateMetadataLabels,
1435+
ValidateFunc: validateMetadataLabels(false),
14361436
},
14371437
"common_annotations": {
14381438
Type: schema.TypeMap,
@@ -1784,7 +1784,7 @@ func applicationSpecSchemaV4(allOptional bool) *schema.Schema {
17841784
Description: "Labels to apply to the namespace.",
17851785
Optional: true,
17861786
Elem: &schema.Schema{Type: schema.TypeString},
1787-
ValidateFunc: validateMetadataLabels,
1787+
ValidateFunc: validateMetadataLabels(isAppSet),
17881788
},
17891789
},
17901790
},

argocd/schema_application_set.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,7 @@ func applicationSetTemplateResource(allOptional bool) *schema.Resource {
10331033
},
10341034
},
10351035
},
1036-
"spec": applicationSpecSchemaV4(allOptional),
1036+
"spec": applicationSpecSchemaV4(allOptional, true),
10371037
},
10381038
}
10391039
}

argocd/schema_cluster.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package argocd
22

33
import (
4-
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
54
"strings"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
67
)
78

89
func clusterSchema() map[string]*schema.Schema {
@@ -226,7 +227,7 @@ func clusterSchema() map[string]*schema.Schema {
226227
Description: "Map of string keys and values that can be used to organize and categorize (scope and select) the cluster secret. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels",
227228
Optional: true,
228229
Elem: &schema.Schema{Type: schema.TypeString},
229-
ValidateFunc: validateMetadataLabels,
230+
ValidateFunc: validateMetadataLabels(false),
230231
},
231232
},
232233
},

argocd/schema_metadata.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func metadataFields(objectName string) map[string]*schema.Schema {
3737
Description: fmt.Sprintf("Map of string keys and values that can be used to organize and categorize (scope and select) the %s. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels", objectName),
3838
Optional: true,
3939
Elem: &schema.Schema{Type: schema.TypeString},
40-
ValidateFunc: validateMetadataLabels,
40+
ValidateFunc: validateMetadataLabels(false),
4141
},
4242
"name": {
4343
Type: schema.TypeString,

argocd/validators.go

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,31 @@ import (
1414
utilValidation "k8s.io/apimachinery/pkg/util/validation"
1515
)
1616

17-
func validateMetadataLabels(value interface{}, key string) (ws []string, es []error) {
18-
m := value.(map[string]interface{})
19-
for k, v := range m {
20-
for _, msg := range utilValidation.IsQualifiedName(k) {
21-
es = append(es, fmt.Errorf("%s (%q) %s", key, k, msg))
22-
}
17+
func validateMetadataLabels(isAppSet bool) func(value interface{}, key string) (ws []string, es []error) {
18+
return func(value interface{}, key string) (ws []string, es []error) {
19+
m := value.(map[string]interface{})
20+
for k, v := range m {
21+
for _, msg := range utilValidation.IsQualifiedName(k) {
22+
es = append(es, fmt.Errorf("%s (%q) %s", key, k, msg))
23+
}
2324

24-
val, isString := v.(string)
25-
if !isString {
26-
es = append(es, fmt.Errorf("%s.%s (%#v): Expected value to be string", key, k, v))
27-
return
28-
}
25+
val, isString := v.(string)
26+
if !isString {
27+
es = append(es, fmt.Errorf("%s.%s (%#v): Expected value to be string", key, k, v))
28+
return
29+
}
2930

30-
for _, msg := range utilValidation.IsValidLabelValue(val) {
31-
es = append(es, fmt.Errorf("%s (%q) %s", key, val, msg))
31+
if isAppSet && strings.HasPrefix(val, "{{") && strings.HasSuffix(val, "}}") {
32+
return
33+
}
34+
35+
for _, msg := range utilValidation.IsValidLabelValue(val) {
36+
es = append(es, fmt.Errorf("%s (%q) %s", key, val, msg))
37+
}
3238
}
33-
}
3439

35-
return
40+
return
41+
}
3642
}
3743

3844
func validateMetadataAnnotations(value interface{}, key string) (ws []string, es []error) {

argocd/validators_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,126 @@ import (
44
"fmt"
55
"reflect"
66
"testing"
7+
8+
"github.com/stretchr/testify/require"
79
)
810

11+
func Test_validateMetadataLabels(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
name string
16+
isAppSet bool
17+
value interface{}
18+
key string
19+
wantWs []string
20+
wantEs []error
21+
}{
22+
{
23+
name: "Valid labels",
24+
isAppSet: false,
25+
value: map[string]interface{}{
26+
"valid-key": "valid-value",
27+
},
28+
key: "metadata_labels",
29+
wantWs: nil,
30+
wantEs: nil,
31+
},
32+
{
33+
name: "Invalid label key",
34+
isAppSet: false,
35+
value: map[string]interface{}{
36+
"Invalid Key!": "valid-value",
37+
},
38+
key: "metadata_labels",
39+
wantWs: nil,
40+
wantEs: []error{
41+
fmt.Errorf("metadata_labels (\"Invalid Key!\") name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
42+
},
43+
},
44+
{
45+
name: "Invalid label value",
46+
isAppSet: false,
47+
value: map[string]interface{}{
48+
"valid-key": "Invalid Value!",
49+
},
50+
key: "metadata_labels",
51+
wantWs: nil,
52+
wantEs: []error{
53+
fmt.Errorf("metadata_labels (\"Invalid Value!\") a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')"),
54+
},
55+
},
56+
{
57+
name: "Non-string label value",
58+
isAppSet: false,
59+
value: map[string]interface{}{
60+
"valid-key": 123,
61+
},
62+
key: "metadata_labels",
63+
wantWs: nil,
64+
wantEs: []error{
65+
fmt.Errorf("metadata_labels.valid-key (123): Expected value to be string"),
66+
},
67+
},
68+
{
69+
name: "Valid templated value for AppSet",
70+
isAppSet: true,
71+
value: map[string]interface{}{
72+
"valid-key": "{{ valid-template }}",
73+
},
74+
key: "metadata_labels",
75+
wantWs: nil,
76+
wantEs: nil,
77+
},
78+
{
79+
name: "Invalid templated value for non-AppSet",
80+
isAppSet: false,
81+
value: map[string]interface{}{
82+
"valid-key": "{{ invalid-template }}",
83+
},
84+
key: "metadata_labels",
85+
wantWs: nil,
86+
wantEs: []error{
87+
fmt.Errorf("metadata_labels (\"{{ invalid-template }}\") a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')"),
88+
},
89+
},
90+
{
91+
name: "Empty label key",
92+
isAppSet: false,
93+
value: map[string]interface{}{
94+
"": "valid-value",
95+
},
96+
key: "metadata_labels",
97+
wantWs: nil,
98+
wantEs: []error{
99+
fmt.Errorf("metadata_labels (\"\") name part must be non-empty"),
100+
fmt.Errorf("metadata_labels (\"\") name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"),
101+
},
102+
},
103+
{
104+
name: "Empty label value",
105+
isAppSet: false,
106+
value: map[string]interface{}{
107+
"valid-key": "",
108+
},
109+
key: "metadata_labels",
110+
wantWs: nil,
111+
wantEs: nil,
112+
},
113+
}
114+
115+
for _, tt := range tests {
116+
t.Run(tt.name, func(t *testing.T) {
117+
t.Parallel()
118+
119+
gotWs, gotEs := validateMetadataLabels(tt.isAppSet)(tt.value, tt.key)
120+
121+
require.Equal(t, tt.wantWs, gotWs)
122+
require.Equal(t, tt.wantEs, gotEs)
123+
})
124+
}
125+
}
126+
9127
func Test_validateSSHPrivateKey(t *testing.T) {
10128
t.Parallel()
11129

0 commit comments

Comments
 (0)