Skip to content

Commit 6a13796

Browse files
chrfwowaepfli
andauthored
feat: accept version numbers which are not strings (#1589)
## This PR Fixes errors when the provided version is not a string but a number (e.g. versions like 1.0, 2, ...) and improves the test suite to use our `parseSemverEvaluationData` function. Furthermore, logging now includes erroneous inputs. --------- Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com> Co-authored-by: Simon Schrottner <simon.schrottner@dynatrace.com>
1 parent 779e8f9 commit 6a13796

File tree

6 files changed

+343
-115
lines changed

6 files changed

+343
-115
lines changed

core/pkg/evaluator/semver.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func parseSemverEvaluationData(values interface{}) (string, string, SemVerOperat
102102
}
103103

104104
if len(parsed) != 3 {
105-
return "", "", "", errors.New("sem_ver evaluation must contain a value, an operator and a comparison target")
105+
return "", "", "", errors.New("sem_ver evaluation must contain a value, an operator, and a comparison target")
106106
}
107107

108108
actualVersion, err := parseSemanticVersion(parsed[0])
@@ -122,19 +122,25 @@ func parseSemverEvaluationData(values interface{}) (string, string, SemVerOperat
122122
return actualVersion, targetVersion, operator, nil
123123
}
124124

125-
func parseSemanticVersion(v interface{}) (string, error) {
126-
version, ok := v.(string)
127-
if !ok {
128-
return "", errors.New("sem_ver evaluation: property did not resolve to a string value")
125+
func ensureString(v interface{}) string {
126+
if str, ok := v.(string); ok {
127+
// It's already a string
128+
return str
129129
}
130+
// Convert to string if not already
131+
return fmt.Sprintf("%v", v)
132+
}
133+
134+
func parseSemanticVersion(v interface{}) (string, error) {
135+
version := ensureString(v)
130136
// version strings are only valid in the semver package if they start with a 'v'
131137
// if it's not present in the given value, we prepend it
132138
if !strings.HasPrefix(version, "v") {
133139
version = "v" + version
134140
}
135141

136142
if !semver.IsValid(version) {
137-
return "", errors.New("not a valid semantic version string")
143+
return "", fmt.Errorf("'%v' is not a valid semantic version string", version)
138144
}
139145

140146
return version, nil
@@ -143,7 +149,7 @@ func parseSemanticVersion(v interface{}) (string, error) {
143149
func parseOperator(o interface{}) (SemVerOperator, error) {
144150
operatorString, ok := o.(string)
145151
if !ok {
146-
return "", errors.New("could not parse operator")
152+
return "", fmt.Errorf("could not parse operator '%v'", o)
147153
}
148154

149155
return SemVerOperator(operatorString), nil

core/pkg/evaluator/semver_test.go

+223-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,76 @@ func TestSemVerOperator_Compare(t *testing.T) {
2323
want bool
2424
wantErr bool
2525
}{
26+
{
27+
name: "invalid version",
28+
svo: Greater,
29+
args: args{
30+
v1: "invalid",
31+
v2: "v1.0.0",
32+
},
33+
want: false,
34+
wantErr: true,
35+
},
36+
{
37+
name: "preview version vs non preview version",
38+
svo: Greater,
39+
args: args{
40+
v1: "v1.0.0-preview.1.2",
41+
v2: "v1.0.0",
42+
},
43+
want: false,
44+
wantErr: false,
45+
},
46+
{
47+
name: "preview version vs preview version",
48+
svo: Greater,
49+
args: args{
50+
v1: "v1.0.0-preview.1.3",
51+
v2: "v1.0.0-preview.1.2",
52+
},
53+
want: true,
54+
wantErr: false,
55+
},
56+
{
57+
name: "no prefixed v left greater",
58+
svo: Greater,
59+
args: args{
60+
v1: "0.0.1",
61+
v2: "v0.0.2",
62+
},
63+
want: false,
64+
wantErr: false,
65+
},
66+
{
67+
name: "no prefixed v right greater",
68+
svo: Greater,
69+
args: args{
70+
v1: "v0.0.1",
71+
v2: "0.0.2",
72+
},
73+
want: false,
74+
wantErr: false,
75+
},
76+
{
77+
name: "no prefixed v right equals",
78+
svo: Equals,
79+
args: args{
80+
v1: "v0.0.1",
81+
v2: "0.0.1",
82+
},
83+
want: true,
84+
wantErr: false,
85+
},
86+
{
87+
name: "no prefixed v both",
88+
svo: Greater,
89+
args: args{
90+
v1: "0.0.1",
91+
v2: "0.0.2",
92+
},
93+
want: false,
94+
wantErr: false,
95+
},
2696
{
2797
name: "invalid operator",
2898
svo: "",
@@ -33,6 +103,16 @@ func TestSemVerOperator_Compare(t *testing.T) {
33103
want: false,
34104
wantErr: true,
35105
},
106+
{
107+
name: "less with large number",
108+
svo: Less,
109+
args: args{
110+
v1: "v1234.0.1",
111+
v2: "v1235.0.2",
112+
},
113+
want: true,
114+
wantErr: false,
115+
},
36116
{
37117
name: "less",
38118
svo: Less,
@@ -43,6 +123,16 @@ func TestSemVerOperator_Compare(t *testing.T) {
43123
want: true,
44124
wantErr: false,
45125
},
126+
{
127+
name: "no minor version",
128+
svo: Less,
129+
args: args{
130+
v1: "v1.0",
131+
v2: "v1.2",
132+
},
133+
want: true,
134+
wantErr: false,
135+
},
46136
{
47137
name: "not less",
48138
svo: Less,
@@ -206,13 +296,20 @@ func TestSemVerOperator_Compare(t *testing.T) {
206296
}
207297
for _, tt := range tests {
208298
t.Run(tt.name, func(t *testing.T) {
209-
got, err := tt.svo.compare(tt.args.v1, tt.args.v2)
299+
var operatorInterface interface{} = string(tt.svo)
300+
actualVersion, targetVersion, operator, err := parseSemverEvaluationData([]interface{}{tt.args.v1, operatorInterface, tt.args.v2})
301+
if err != nil {
302+
require.Truef(t, tt.wantErr, "Error parsing semver evaluation data. actualVersion: %s, targetVersion: %s, operator: %s, err: %s", actualVersion, targetVersion, operator, err)
303+
return
304+
}
305+
306+
got, err := operator.compare(actualVersion, targetVersion)
210307

211308
if tt.wantErr {
212309
require.NotNil(t, err)
213310
} else {
214311
require.Nil(t, err)
215-
require.Equalf(t, tt.want, got, "compare(%v, %v)", tt.args.v1, tt.args.v2)
312+
require.Equalf(t, tt.want, got, "compare(%v, %v) operator: %s", tt.args.v1, tt.args.v2, operator)
216313
}
217314
})
218315
}
@@ -385,6 +482,130 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) {
385482
expectedValue: "#FF0000",
386483
expectedReason: model.TargetingMatchReason,
387484
},
485+
"versions given as double - match": {
486+
flags: Flags{
487+
Flags: map[string]model.Flag{
488+
"headerColor": {
489+
State: "ENABLED",
490+
DefaultVariant: "red",
491+
Variants: map[string]any{
492+
"red": "#FF0000",
493+
"blue": "#0000FF",
494+
"green": "#00FF00",
495+
"yellow": "#FFFF00",
496+
},
497+
Targeting: []byte(`{
498+
"if": [
499+
{
500+
"sem_ver": [1.2, "=", "1.2"]
501+
},
502+
"red", "green"
503+
]
504+
}`),
505+
},
506+
},
507+
},
508+
flagKey: "headerColor",
509+
context: map[string]any{
510+
"version": "1.0.0",
511+
},
512+
expectedVariant: "red",
513+
expectedValue: "#FF0000",
514+
expectedReason: model.TargetingMatchReason,
515+
},
516+
"versions given as int - match": {
517+
flags: Flags{
518+
Flags: map[string]model.Flag{
519+
"headerColor": {
520+
State: "ENABLED",
521+
DefaultVariant: "red",
522+
Variants: map[string]any{
523+
"red": "#FF0000",
524+
"blue": "#0000FF",
525+
"green": "#00FF00",
526+
"yellow": "#FFFF00",
527+
},
528+
Targeting: []byte(`{
529+
"if": [
530+
{
531+
"sem_ver": [1, "=", "v1.0.0"]
532+
},
533+
"red", "green"
534+
]
535+
}`),
536+
},
537+
},
538+
},
539+
flagKey: "headerColor",
540+
context: map[string]any{
541+
"version": "1.0.0",
542+
},
543+
expectedVariant: "red",
544+
expectedValue: "#FF0000",
545+
expectedReason: model.TargetingMatchReason,
546+
},
547+
"versions and minor-version without patch version operator provided - match": {
548+
flags: Flags{
549+
Flags: map[string]model.Flag{
550+
"headerColor": {
551+
State: "ENABLED",
552+
DefaultVariant: "red",
553+
Variants: map[string]any{
554+
"red": "#FF0000",
555+
"blue": "#0000FF",
556+
"green": "#00FF00",
557+
"yellow": "#FFFF00",
558+
},
559+
Targeting: []byte(`{
560+
"if": [
561+
{
562+
"sem_ver": [1.2, "=", "1.2"]
563+
},
564+
"red", "green"
565+
]
566+
}`),
567+
},
568+
},
569+
},
570+
flagKey: "headerColor",
571+
context: map[string]any{
572+
"version": "1.0.0",
573+
},
574+
expectedVariant: "red",
575+
expectedValue: "#FF0000",
576+
expectedReason: model.TargetingMatchReason,
577+
},
578+
"versions with prefixed v operator provided - match": {
579+
flags: Flags{
580+
Flags: map[string]model.Flag{
581+
"headerColor": {
582+
State: "ENABLED",
583+
DefaultVariant: "red",
584+
Variants: map[string]any{
585+
"red": "#FF0000",
586+
"blue": "#0000FF",
587+
"green": "#00FF00",
588+
"yellow": "#FFFF00",
589+
},
590+
Targeting: []byte(`{
591+
"if": [
592+
{
593+
"sem_ver": [{"var": "version"}, "<", "v1.2"]
594+
},
595+
"red", "green"
596+
]
597+
}`),
598+
},
599+
},
600+
},
601+
flagKey: "headerColor",
602+
context: map[string]any{
603+
"version": "v1.0.0",
604+
},
605+
expectedVariant: "red",
606+
expectedValue: "#FF0000",
607+
expectedReason: model.TargetingMatchReason,
608+
},
388609
"versions and major-version operator provided - no match": {
389610
flags: Flags{
390611
Flags: map[string]model.Flag{
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
-----BEGIN CERTIFICATE-----
2-
MIIFJTCCAw2gAwIBAgIUHkLmoT3U1jDYbXAqX84fjR/Qw5kwDQYJKoZIhvcNAQEL
3-
BQAwITEfMB0GA1UEAwwWZmxhZ0QgdGVzdCBjZXJ0aWZpY2F0ZTAgFw0yNTAxMDgw
4-
OTI0MThaGA8yMDUyMDUyNTA5MjQxOFowITEfMB0GA1UEAwwWZmxhZ0QgdGVzdCBj
5-
ZXJ0aWZpY2F0ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALQK8r5J
6-
7wOvdMsbGy63wlOie97lMHT/HSfneyTEnvRM4ZenKNjrKI6zlyYMQX9/RQs9qCxF
7-
QQDFMhLoMrQKkDptVur+PhYCa/uAWUtTFVXJMbJ2Zdzbg86WCgOlRLwMqGiwnu0l
8-
25dk5Ja5nE4HSSg5SipLxtCCdxT3n5YmQBO+Kkyc1Uxgk6yQMQcdQENGhQHazVzy
9-
fAsrutJFZNtkYXObR9t6/MyYgg3zFjNRCpOYhc9misT54TVteb9L0smLqMhVfQkh
10-
CFWkVEWmaeGyTYAR7gGvFy9b7N/45FfBvlumgR7KiG/uLJNadCQtf7v3pRN36SBH
11-
aZleAp8KWmy6N2IMuWx/hMQCVnzoi05gvYkCWJSoseobhiwaXsDFQCc8ZB/c2H9C
12-
yMmyKRL+c3RM7lI/InxLmpS94xJIhDuvDiEPYTWqY3BPqCvAly8LXEYBqgJNsnOa
13-
+pdoJQ/rl87pIdDt3CK/wWQ1GIlp1v7aYY53riM4i0Hvnk6SPNBgpUXbqAZjpnOb
14-
fsIrKGO/FAJ5Hc3mfiuTj15jDoxPPUPqzj4yP2h7WqbdYwEpDvTd0Cd+tjgCuQUj
15-
JU2MpTcNUQQQFdXASZQgfXHOTW873zIx4mourcO7jkEDwId13H9QMU4JPG9ZsLG6
16-
p08S26yx8G0chV/Q7BNVfOTCKAc8GH+94CynAgMBAAGjUzBRMB0GA1UdDgQWBBQs
17-
ubjWSGXffy7mMQGV2EdbdUwtPjAfBgNVHSMEGDAWgBQsubjWSGXffy7mMQGV2Edb
18-
dUwtPjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBwJyNT3ldS
19-
DsvOOFH5VT+yMByXv690FH7aYBu5VeSpd8oZPJZgzRh58fTzUxrnPk7iyuQzlgCw
20-
ZZssvObDEA1eK9vG1svomY8yu8VrpKG/yl0YItnZpuQTvCfJsNOm1Qe0j8FPJVOu
21-
Jgges3i+QV18o2dmtQKTTWdExu2J30eQgL8SB7hG9vE6tfcVkzCw6+5ev+fyO9kd
22-
DxrN9YY/FKjNMY2WSCjbQ99NFNS3Qf0MFLLy+V3oiYlT1/qkuX9omLIL6qDabuXE
23-
8XbppIG+ku610hiVRd4ZZyTH1EIThqevs7y3zQYFQNDL8wPfYHRTl00Fto/NLcTH
24-
+cAhf5nC5R4PqvSJZ1IZkxUj/8oljUtMsbFKTekgJGnP/VzDFoNmYO6lW3rBFM1L
25-
ovFJhaesnAKQ6WFk+Yru9kFylX2dHqFsB0SlggGS+r8kwbrXlXJhWAE/WpPVyU+H
26-
IUyYluNLKKvMBYa9GqUC6C7XlZmiuemZy0oCy4gHqdmII09Bj3C3nT7+Ms4D21jh
27-
fZKN4WZcpsA2XaE27XsAqJp2gIV+SL3VqD3hAOFIRWAwQVvTlVJ1GWM1DJBVnLpp
28-
BFK4mO0Mga0AbzJ6VL9ElvXVjqwRl8gbeT7yHWQ8A0r7yl4n0i4FloDeoCmr7i9O
29-
9dqUWaknNa6rIUKwvIodkn39C1G7uCpeLw==
2+
MIIFJTCCAw2gAwIBAgIUKd5pSsJ6Fxr2f4vF2a+MOlQAvcowDQYJKoZIhvcNAQEL
3+
BQAwITEfMB0GA1UEAwwWZmxhZ0QgdGVzdCBjZXJ0aWZpY2F0ZTAgFw0yNTAzMTIx
4+
MjEzNDdaGA8yMDUyMDcyNzEyMTM0N1owITEfMB0GA1UEAwwWZmxhZ0QgdGVzdCBj
5+
ZXJ0aWZpY2F0ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALqvg928
6+
yzEpRGpbK/gQiVC/tH3CzrieP+QHDI9D0Hlzi8F2V9YxYbmGutewd3mkNiiaVfuo
7+
Ue5HRYwhMBKiMhUByZc2a5wF/eftPE2Hj5rDFiOnW5duDERLMLjFE4lOwnuCZtNk
8+
Ljt5FEd7Q6TbDfOW4ETxwdbQObS+neSXmqYrWQvdIvN4jKyHkiMqdMwGZp6AYYMz
9+
FVEKEQ76xbNkTiMjhOfaCZklZp4D99DNIANtYcpf3+VRZcN4xkVe7+wP8ofioZ/M
10+
CwAQAW/2gq2eienTQ+XGHUZfig0JslinuTZy15wr+1lnYzNvJtK/30Z+pmhjkQBN
11+
f2d0Be6Gf3RUTEFlXqysY1aDEbvW+8lSKyuLr/H73O1vGkhMfJ1A5dak+vwhk40g
12+
8twYSJesDGjNNW/rxglSZcCA1sPu39gLC+FHZ3eTnur5JOwLvM7n0sn1Ztmkq//7
13+
eCY+ZDT/X39UwluMa9SFugdqfqOLpCCZtkMCLxjNoDLPmEqCHS5E2q3yrl6RzNlg
14+
yc78dRaChgTQbbS2EX0TXqCDnNpyuQAl9hwcMbh1Law0iNGuRircZW6v4Mkc6se0
15+
rpDmtGy9E/wrr2XGsD5KtjpVF2rUHXvpzoY+ioOtiOrDdnLBAwRyw8bIsynLlbpb
16+
F8K7H5b7x1RI8d9AAZohEl9c9iqMyvJnaZTrAgMBAAGjUzBRMB0GA1UdDgQWBBTj
17+
lAwMUauC+x+73IzYJOxNnqJnMDAfBgNVHSMEGDAWgBTjlAwMUauC+x+73IzYJOxN
18+
nqJnMDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBWGKra39+p
19+
WGpobGClcmlP+n5umtzOolFNTM5OyVmNaqWLtcigldFwpPFW7lhYIbbmxWSDpFto
20+
DDkv1FuiEulo7C4TVEa6x/RsgCfOMR5WCsXOh65moSN86SFMPbnSuajpGuY5RfqH
21+
wxfAyd9/IO4aqwINQE0P4VNgqhOMJ66LmodPsGjXC2mkLDePV3sswka+dv1CtFWU
22+
/9Zp7n0UwEga3JLw5RTnXswpA5lyIkRnda9m9ydL5A5uKe3tCwSEUOUGphjdi6Y0
23+
ycAwsD1G7XwK60seZyX3MUMEo4CiH21WC3P2c8xRVl7TxQEj2m+NlQ3UArgIdl60
24+
4FE7p+yorrXEi43fwLHFi17P7LGvnurp9H6Xs5YZPGkYwEOD6HxDpGHaYlvHArDe
25+
r2+pw/o8YJqP3E9YjVK5wby4v32DdBSGykyJTtXWj5JkwmILUzTE0skDk7fviiyO
26+
F5nwMt37bSliU5p8mk9YzJNv+tTqRGEsOj3NjnjVX1BSDvuKeZfiLG08LRaQrhWa
27+
6ZU+BPtTi0JkeczEpreSNMPnKytGRkXQ3AXhkeQYLBoAbypzp0zzw6yJ6ADBbUZu
28+
Kn3iuQR7nqpdnE7hNh21mO7tKGV0il4ePQUIeM+E3MJNW4KiSN5UV+Z9GxeQQudD
29+
zJCP+qbCnwOTm+ZDZOymqCoMILaWVL0/Qw==
3030
-----END CERTIFICATE-----

flagd/pkg/service/flag-sync/test-cert/gen.sh

100644100755
+1-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ openssl req -x509 -newkey rsa:4096 -days 9999 -nodes -keyout ca-key.pem -out ca-
1313
openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/CN=flagD test server PR and CSR"
1414

1515
# 3. Use CA's private key to sign web server's CSR and get back the signed certificate
16-
openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf
16+
openssl x509 -req -in server-req.pem -days 9999 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf
1717

0 commit comments

Comments
 (0)