Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.25.0
require (
cuelang.org/go v0.11.1
github.com/go-git/go-billy/v6 v6.0.0-alpha.2
github.com/gobwas/glob v0.2.3
github.com/gobwas/glob v1.0.0
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
)
Expand Down
2 changes: 2 additions & 0 deletions core/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/gobwas/glob v1.0.0 h1:p+FKbLEIsK1yZ39/OINwFvqNb5oyPY4H8xcy6uYu8dg=
github.com/gobwas/glob v1.0.0/go.mod h1:oWCdo522i2P1n/hMXGNWs7yoV4wy/ciZuUIbvKj5rkc=
17 changes: 17 additions & 0 deletions core/validation/flipt.cue
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,21 @@ close({
}
description?: string | null
operator: "isoneof" | "isnotoneof"
} | {
type: "SEMVER_COMPARISON_TYPE"
property: string & =~"^.+$"
value?: string
description?: string | null
operator: "eq" | "neq" | "present" | "notpresent" | "lt" | "lte" | "gt" | "gte"
} | {
type: "SEMVER_COMPARISON_TYPE"
property: string & =~"^.+$"
if list.Contains(#versions[:6], version) { // before 1.6
value: string
}
if !list.Contains(#versions[:6], version) { // 1.6 or later
value: [...string]
}
description?: string | null
operator: "isoneof" | "isnotoneof"
})
120 changes: 119 additions & 1 deletion core/validation/flipt.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,106 @@
}
]
},
"semverComparisonOperator": {
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "SEMVER_COMPARISON_TYPE"
}
},
"required": [
"operator"
]
},
"then": {
"properties": {
"operator": {
"enum": [
"eq",
"neq",
"present",
"notpresent",
"lt",
"lte",
"gt",
"gte",
"isoneof",
"isnotoneof"
]
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "SEMVER_COMPARISON_TYPE"
},
"operator": {
"not": {
"enum": [
"isoneof",
"isnotoneof"
]
}
}
},
"required": [
"type"
]
},
"then": {
"properties": {
"value": {
"type": "string"
}
}
}
},
{
"if": {
"properties": {
"type": {
"const": "SEMVER_COMPARISON_TYPE"
},
"operator": {
"enum": [
"isoneof",
"isnotoneof"
]
}
},
"required": [
"type",
"operator"
]
},
"then": {
"properties": {
"value": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"required": [
"value"
]
}
}
]
},
"constraint": {
"required": [
"type",
Expand All @@ -520,7 +620,8 @@
"NUMBER_COMPARISON_TYPE",
"BOOLEAN_COMPARISON_TYPE",
"DATETIME_COMPARISON_TYPE",
"ENTITY_ID_COMPARISON_TYPE"
"ENTITY_ID_COMPARISON_TYPE",
"SEMVER_COMPARISON_TYPE"
]
},
"property": {
Expand Down Expand Up @@ -564,6 +665,9 @@
},
{
"$ref": "#/$defs/entityIdComparisonOperator"
},
{
"$ref": "#/$defs/semverComparisonOperator"
}
]
},
Expand Down Expand Up @@ -610,6 +714,20 @@
"value": { "type": "array", "items": { "type": "string" } }
}
}
},
{
"if": {
"properties": {
"type": { "const": "SEMVER_COMPARISON_TYPE" },
"operator": { "enum": ["isoneof", "isnotoneof"] }
},
"required": ["type", "operator"]
},
"then": {
"properties": {
"value": { "type": "array", "items": { "type": "string" } }
}
}
}
]
},
Expand Down
16 changes: 16 additions & 0 deletions core/validation/testdata/valid_constraint_semver_operator.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: "1.2"
namespace: default
segments:
- key: semver-users
name: Semver Users
description: Users matched with semantic version comparisons.
match_type: ALL_MATCH_TYPE
constraints:
- type: SEMVER_COMPARISON_TYPE
property: app_version
operator: gte
value: "2.1.0-alpha.1"
- type: SEMVER_COMPARISON_TYPE
property: client_version
operator: lt
value: "3.0.0"
14 changes: 14 additions & 0 deletions core/validation/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ func TestValidate_ConstraintLTOperator(t *testing.T) {
assert.NoError(t, err)
}

func TestValidate_ConstraintSemverOperator(t *testing.T) {
const file = "testdata/valid_constraint_semver_operator.yaml"
f, err := os.Open(file)
require.NoError(t, err)

defer f.Close()

v, err := NewFeaturesValidator()
require.NoError(t, err)

err = v.Validate(file, f)
assert.NoError(t, err)
}

func TestValidate_DefaultVariant_V3(t *testing.T) {
const file = "testdata/valid_default_variant_v3.yaml"
f, err := os.Open(file)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.5.0
github.com/ClickHouse/clickhouse-go/v2 v2.48.0
github.com/MakeNowJust/heredoc v1.0.0
github.com/Masterminds/semver/v3 v3.5.0
github.com/ProtonMail/go-crypto v1.5.2
github.com/aws/aws-sdk-go-v2 v1.47.1
github.com/aws/aws-sdk-go-v2/config v1.33.6
Expand Down
4 changes: 3 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:yqV2xTwBQgBgDqYHkCZ0IZ/8luupq476yKdcuybb8xE=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
Expand Down Expand Up @@ -950,3 +950,5 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
99 changes: 99 additions & 0 deletions internal/server/evaluation/evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package evaluation

import (
"context"
"encoding/json"
"fmt"
"hash/crc32"
"sort"
"strconv"
"strings"
"time"

"github.com/Masterminds/semver/v3"

errs "go.flipt.io/flipt/errors"
environments "go.flipt.io/flipt/internal/server/environments"
"go.flipt.io/flipt/internal/server/metrics"
Expand Down Expand Up @@ -657,6 +660,8 @@ func (s *Server) matchConstraints(evalCtx map[string]string, constraints []stora
match, err = matchesDateTime(c, v)
case core.ComparisonType_ENTITY_ID_COMPARISON_TYPE:
match, err = matchesString(c, entityId)
case core.ComparisonType_SEMVER_COMPARISON_TYPE:
match, err = matchesSemver(c, v)
default:
return false, reason, errs.ErrInvalid("unknown constraint type")
}
Expand Down Expand Up @@ -894,6 +899,100 @@ func tryParseDateTime(v string) (time.Time, error) {
return time.Time{}, errs.ErrInvalidf("parsing datetime from %q", v)
}

func matchesSemver(c storage.EvaluationConstraint, v string) (bool, error) {
switch c.Operator {
case flipt.OpNotPresent:
return len(strings.TrimSpace(v)) == 0, nil
case flipt.OpPresent:
return len(strings.TrimSpace(v)) != 0, nil
}

// can't parse an empty string
if v == "" {
return false, nil
}

d, err := semver.NewVersion(strings.TrimSpace(v))
if err != nil {
return false, errs.ErrInvalidf("parsing semver from %q", v)
}

switch c.Operator {
case flipt.OpIsOneOf:
if c.SemverList != nil {
for _, target := range c.SemverList {
if d.Equal(target) {
return true, nil
}
}
return false, nil
}
var values []string
if err := json.Unmarshal([]byte(c.Value), &values); err != nil {
return false, errs.ErrInvalidf("invalid value for constraint %q", c.Value)
}
for _, val := range values {
target, err := semver.NewVersion(strings.TrimSpace(val))
if err != nil {
return false, errs.ErrInvalidf("parsing semver from %q", val)
}
if d.Equal(target) {
return true, nil
}
}
return false, nil
case flipt.OpIsNotOneOf:
if c.SemverList != nil {
for _, target := range c.SemverList {
if d.Equal(target) {
return false, nil
}
}
return true, nil
}
var values []string
if err := json.Unmarshal([]byte(c.Value), &values); err != nil {
return false, errs.ErrInvalidf("invalid value for constraint %q", c.Value)
}
for _, val := range values {
target, err := semver.NewVersion(strings.TrimSpace(val))
if err != nil {
return false, errs.ErrInvalidf("parsing semver from %q", val)
}
if d.Equal(target) {
return false, nil
}
}
return true, nil
}

value := c.Semver
if value == nil {
var err error
value, err = semver.NewVersion(strings.TrimSpace(c.Value))
if err != nil {
return false, errs.ErrInvalidf("parsing semver from %q", c.Value)
}
}

switch c.Operator {
case flipt.OpEQ:
return d.Equal(value), nil
case flipt.OpNEQ:
return !d.Equal(value), nil
case flipt.OpLT:
return d.LessThan(value), nil
case flipt.OpLTE:
return d.LessThan(value) || d.Equal(value), nil
case flipt.OpGT:
return d.GreaterThan(value), nil
case flipt.OpGTE:
return d.GreaterThan(value) || d.Equal(value), nil
}

return false, nil
}

func getVariant(key string, variants ...*core.Variant) (*core.Variant, bool) {
for _, v := range variants {
if v.Key == key {
Expand Down
Loading
Loading