diff --git a/build/testing/cli.go b/build/testing/cli.go index 1f2d47466e..97b5c96f52 100644 --- a/build/testing/cli.go +++ b/build/testing/cli.go @@ -457,10 +457,15 @@ segments: value: buzz ` - expectedYAMLStreamOutput = `version: "1.3" -namespace: default + expectedYAMLStreamOutput = `version: "1.4" +namespace: + key: default + name: Default + description: Default namespace --- -namespace: foo +namespace: + key: foo + name: foo flags: - key: zUFtS7D0UyMeueYu name: UAoZRksg94r1iipa @@ -491,7 +496,9 @@ segments: value: buzz match_type: ALL_MATCH_TYPE --- -namespace: bar +namespace: + key: bar + name: bar flags: - key: zUFtS7D0UyMeueYu name: UAoZRksg94r1iipa diff --git a/build/testing/integration.go b/build/testing/integration.go index e965e294bd..1d6c9f9877 100644 --- a/build/testing/integration.go +++ b/build/testing/integration.go @@ -14,6 +14,7 @@ import ( "math/big" "os" "path" + "runtime" "strings" "time" @@ -46,7 +47,7 @@ func init() { var ( protocolPorts = map[string]int{"http": 8080, "grpc": 9000} replacer = strings.NewReplacer(" ", "-", "/", "-") - sema = make(chan struct{}, 6) + sema = make(chan struct{}, max(6, runtime.NumCPU())) // AllCases are the top-level filterable integration test cases. AllCases = map[string]testCaseFn{ @@ -627,11 +628,6 @@ func importExport(ctx context.Context, _ *dagger.Client, base, flipt *dagger.Con return err } - if ns == "default" { - // replace namespace in expected yaml - expected = strings.ReplaceAll(expected, "version: \"1.3\"\n", fmt.Sprintf("version: \"1.3\"\nnamespace: %s\n", ns)) - } - if ns != "default" { flags = append(flags, "--namespaces", ns) } diff --git a/build/testing/integration/readonly/testdata/main/default.yaml b/build/testing/integration/readonly/testdata/main/default.yaml index ad6c48ae14..bb2b4ae031 100644 --- a/build/testing/integration/readonly/testdata/main/default.yaml +++ b/build/testing/integration/readonly/testdata/main/default.yaml @@ -1,4 +1,8 @@ -version: "1.3" +version: "1.4" +namespace: + key: default + name: Default + description: Default namespace flags: - key: flag_001 name: FLAG_001 diff --git a/build/testing/integration/readonly/testdata/main/production.yaml b/build/testing/integration/readonly/testdata/main/production.yaml index 83a044dbc7..733bb25eba 100644 --- a/build/testing/integration/readonly/testdata/main/production.yaml +++ b/build/testing/integration/readonly/testdata/main/production.yaml @@ -1,5 +1,7 @@ -version: "1.3" -namespace: production +version: "1.4" +namespace: + key: production + name: Production flags: - key: flag_001 name: FLAG_001 diff --git a/build/testing/testdata/flipt-namespace-foo.yml b/build/testing/testdata/flipt-namespace-foo.yml index 96a90014c6..9ae3c6a526 100644 --- a/build/testing/testdata/flipt-namespace-foo.yml +++ b/build/testing/testdata/flipt-namespace-foo.yml @@ -1,30 +1,29 @@ -# exported by Flipt (dev) on 2020-02-24T14:57:19Z namespace: foo flags: -- key: zUFtS7D0UyMeueYu - name: UAoZRksg94r1iipa - description: description - enabled: true - variants: - - key: NGxfcVffpMhBz9n8 - name: fhDHQ7rcxvoaWbHw - - key: sDGD6NvfCRyaQUn3 - rules: - - segment: 08UoVJ96LhZblPEx - rank: 1 - distributions: - - variant: NGxfcVffpMhBz9n8 - rollout: 100 + - key: zUFtS7D0UyMeueYu + name: UAoZRksg94r1iipa + description: description + enabled: true + variants: + - key: NGxfcVffpMhBz9n8 + name: fhDHQ7rcxvoaWbHw + - key: sDGD6NvfCRyaQUn3 + rules: + - segment: 08UoVJ96LhZblPEx + rank: 1 + distributions: + - variant: NGxfcVffpMhBz9n8 + rollout: 100 segments: -- key: 08UoVJ96LhZblPEx - name: 2oS8SHbrxyFkRg1a - description: description - constraints: - - type: STRING_COMPARISON_TYPE - property: foo - operator: eq - value: baz - - type: STRING_COMPARISON_TYPE - property: fizz - operator: neq - value: buzz + - key: 08UoVJ96LhZblPEx + name: 2oS8SHbrxyFkRg1a + description: description + constraints: + - type: STRING_COMPARISON_TYPE + property: foo + operator: eq + value: baz + - type: STRING_COMPARISON_TYPE + property: fizz + operator: neq + value: buzz diff --git a/build/testing/testdata/flipt-yaml-stream.yml b/build/testing/testdata/flipt-yaml-stream.yml index fbce3efd6b..031f64cc88 100644 --- a/build/testing/testdata/flipt-yaml-stream.yml +++ b/build/testing/testdata/flipt-yaml-stream.yml @@ -1,59 +1,59 @@ namespace: foo flags: -- key: zUFtS7D0UyMeueYu - name: UAoZRksg94r1iipa - description: description - enabled: true - variants: - - key: NGxfcVffpMhBz9n8 - name: fhDHQ7rcxvoaWbHw - - key: sDGD6NvfCRyaQUn3 - rules: - - segment: 08UoVJ96LhZblPEx - rank: 1 - distributions: - - variant: NGxfcVffpMhBz9n8 - rollout: 100 + - key: zUFtS7D0UyMeueYu + name: UAoZRksg94r1iipa + description: description + enabled: true + variants: + - key: NGxfcVffpMhBz9n8 + name: fhDHQ7rcxvoaWbHw + - key: sDGD6NvfCRyaQUn3 + rules: + - segment: 08UoVJ96LhZblPEx + rank: 1 + distributions: + - variant: NGxfcVffpMhBz9n8 + rollout: 100 segments: -- key: 08UoVJ96LhZblPEx - name: 2oS8SHbrxyFkRg1a - description: description - constraints: - - type: STRING_COMPARISON_TYPE - property: foo - operator: eq - value: baz - - type: STRING_COMPARISON_TYPE - property: fizz - operator: neq - value: buzz + - key: 08UoVJ96LhZblPEx + name: 2oS8SHbrxyFkRg1a + description: description + constraints: + - type: STRING_COMPARISON_TYPE + property: foo + operator: eq + value: baz + - type: STRING_COMPARISON_TYPE + property: fizz + operator: neq + value: buzz --- namespace: bar flags: -- key: zUFtS7D0UyMeueYu - name: UAoZRksg94r1iipa - description: description - enabled: true - variants: - - key: NGxfcVffpMhBz9n8 - name: fhDHQ7rcxvoaWbHw - - key: sDGD6NvfCRyaQUn3 - rules: - - segment: 08UoVJ96LhZblPEx - rank: 1 - distributions: - - variant: NGxfcVffpMhBz9n8 - rollout: 100 + - key: zUFtS7D0UyMeueYu + name: UAoZRksg94r1iipa + description: description + enabled: true + variants: + - key: NGxfcVffpMhBz9n8 + name: fhDHQ7rcxvoaWbHw + - key: sDGD6NvfCRyaQUn3 + rules: + - segment: 08UoVJ96LhZblPEx + rank: 1 + distributions: + - variant: NGxfcVffpMhBz9n8 + rollout: 100 segments: -- key: 08UoVJ96LhZblPEx - name: 2oS8SHbrxyFkRg1a - description: description - constraints: - - type: STRING_COMPARISON_TYPE - property: foo - operator: eq - value: baz - - type: STRING_COMPARISON_TYPE - property: fizz - operator: neq - value: buzz + - key: 08UoVJ96LhZblPEx + name: 2oS8SHbrxyFkRg1a + description: description + constraints: + - type: STRING_COMPARISON_TYPE + property: foo + operator: eq + value: baz + - type: STRING_COMPARISON_TYPE + property: fizz + operator: neq + value: buzz diff --git a/build/testing/testdata/flipt.yml b/build/testing/testdata/flipt.yml index af79483d87..495d10b013 100644 --- a/build/testing/testdata/flipt.yml +++ b/build/testing/testdata/flipt.yml @@ -1,30 +1,28 @@ -# exported by Flipt (dev) on 2020-02-24T14:57:19Z - flags: -- key: zUFtS7D0UyMeueYu - name: UAoZRksg94r1iipa - description: description - enabled: true - variants: - - key: NGxfcVffpMhBz9n8 - name: fhDHQ7rcxvoaWbHw - - key: sDGD6NvfCRyaQUn3 - rules: - - segment: 08UoVJ96LhZblPEx - rank: 1 - distributions: - - variant: NGxfcVffpMhBz9n8 - rollout: 100 + - key: zUFtS7D0UyMeueYu + name: UAoZRksg94r1iipa + description: description + enabled: true + variants: + - key: NGxfcVffpMhBz9n8 + name: fhDHQ7rcxvoaWbHw + - key: sDGD6NvfCRyaQUn3 + rules: + - segment: 08UoVJ96LhZblPEx + rank: 1 + distributions: + - variant: NGxfcVffpMhBz9n8 + rollout: 100 segments: -- key: 08UoVJ96LhZblPEx - name: 2oS8SHbrxyFkRg1a - description: description - constraints: - - type: STRING_COMPARISON_TYPE - property: foo - operator: eq - value: baz - - type: STRING_COMPARISON_TYPE - property: fizz - operator: neq - value: buzz + - key: 08UoVJ96LhZblPEx + name: 2oS8SHbrxyFkRg1a + description: description + constraints: + - type: STRING_COMPARISON_TYPE + property: foo + operator: eq + value: baz + - type: STRING_COMPARISON_TYPE + property: fizz + operator: neq + value: buzz diff --git a/core/validation/extended.cue b/core/validation/extended.cue index 8b20fb2578..44cbb2a1a1 100644 --- a/core/validation/extended.cue +++ b/core/validation/extended.cue @@ -1,4 +1,3 @@ #Flag: { description: =~"^.+$" } - diff --git a/core/validation/flipt.cue b/core/validation/flipt.cue index 820f0c0d4e..9230888c4d 100644 --- a/core/validation/flipt.cue +++ b/core/validation/flipt.cue @@ -1,11 +1,18 @@ -version: "1.0" | "1.1" | "1.2" | *"1.3" +version: "1.0" | "1.1" | "1.2" | "1.3" | *"1.4" close({ - namespace: string & =~"^[-_,A-Za-z0-9]+$" | *"default" + version: version + namespace?: #Namespace flags: [...#Flag] segments: [...#Segment] }) +#Namespace: { + key: string & =~"^[-_,A-Za-z0-9]+$" | *"default" + name?: string & =~"^.+$" + description?: string +} | string & =~"^[-_,A-Za-z0-9]+$" + #Flag: { key: string & =~"^[-_,A-Za-z0-9]+$" name: string & =~"^.+$" @@ -13,11 +20,11 @@ close({ enabled: bool | *false variants: [...#Variant] rules: [...#Rule] - if version == "1.1" || version == "1.2" || version == "1.3" { + if version == "1.1" || version == "1.2" || version == "1.3" || version == "1.4" { type: "BOOLEAN_FLAG_TYPE" | *"VARIANT_FLAG_TYPE" #FlagBoolean | *{} } - if version == "1.3" { + if version == "1.3" || version == "1.4" { metadata: [string]: (string | int | bool | float) } } @@ -34,8 +41,8 @@ close({ key: string & =~"^.+$" name?: string & =~"^.+$" description?: string - attachment: {...} | [...] | *null - if version == "1.3" { + attachment: {...} | [...] | *null + if version == "1.3" || version == "1.4" { default: bool | *false } } diff --git a/core/validation/testdata/valid.yaml b/core/validation/testdata/valid.yaml index 7c7f0f4db8..d594af565b 100644 --- a/core/validation/testdata/valid.yaml +++ b/core/validation/testdata/valid.yaml @@ -1,4 +1,7 @@ -namespace: default +namespace: + key: foo + name: foo + description: foo namespace flags: - key: flipt name: flipt diff --git a/core/validation/testdata/valid_namespace_details_v4.yaml b/core/validation/testdata/valid_namespace_details_v4.yaml new file mode 100644 index 0000000000..7ca70e9232 --- /dev/null +++ b/core/validation/testdata/valid_namespace_details_v4.yaml @@ -0,0 +1,96 @@ +version: "1.4" +namespace: + key: default + name: default + description: default namespace +flags: +- key: flipt + name: flipt + description: flipt + enabled: false + variants: + - key: flipt + name: flipt + - key: flipt + name: flipt + default: true + rules: + - segment: internal-users + distributions: + - variant: fromFlipt + rollout: 100 + - segment: all-users + distributions: + - variant: fromFlipt2 + rollout: 100 + metadata: + label: ui + area: 32 + hidden: true + radar: 3.2 +segments: +- key: all-users + name: All Users + description: All Users + match_type: ALL_MATCH_TYPE +- key: internal-users + name: Internal Users + description: All internal users at flipt. + constraints: + - type: STRING_COMPARISON_TYPE + property: organization + operator: eq + value: flipt + match_type: ALL_MATCH_TYPE +--- +namespace: + key: foo + name: foo + description: foo namespace +flags: +- key: flipt + name: flipt + description: flipt + enabled: false + variants: + - key: flipt + name: flipt + - key: flipt + name: flipt + description: I'm a description. + rules: + - segment: internal-users + distributions: + - variant: fromFlipt + rollout: 100 + - segment: all-users + distributions: + - variant: fromFlipt2 + rollout: 100 +- key: boolean + name: Boolean + description: Boolean flag + enabled: false + rollouts: + - description: enabled for internal users + segment: + key: internal-users + value: true + - description: enabled for 50% + threshold: + percentage: 50.0 + value: true +segments: +- key: all-users + name: All Users + description: All Users + match_type: ALL_MATCH_TYPE +- key: internal-users + name: Internal Users + description: All internal users at flipt. + constraints: + - type: STRING_COMPARISON_TYPE + property: organization + operator: eq + value: flipt + match_type: ALL_MATCH_TYPE \ No newline at end of file diff --git a/core/validation/testdata/valid_yaml_stream.yaml b/core/validation/testdata/valid_yaml_stream.yaml index f74c0297f5..33c8fde541 100644 --- a/core/validation/testdata/valid_yaml_stream.yaml +++ b/core/validation/testdata/valid_yaml_stream.yaml @@ -1,4 +1,6 @@ -namespace: default +namespace: + key: default + name: default flags: - key: flipt name: flipt @@ -47,7 +49,8 @@ segments: value: flipt match_type: ALL_MATCH_TYPE --- -namespace: foo +namespace: + key: foo flags: - key: flipt name: flipt diff --git a/core/validation/validate_test.go b/core/validation/validate_test.go index 9f48f18360..5c384e62ab 100644 --- a/core/validation/validate_test.go +++ b/core/validation/validate_test.go @@ -79,6 +79,20 @@ func TestValidate_Metadata_V3(t *testing.T) { assert.NoError(t, err) } +func TestValidate_NamespaceDetails_v4(t *testing.T) { + const file = "testdata/valid_namespace_details_v4.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_YAML_Stream(t *testing.T) { const file = "testdata/valid_yaml_stream.yaml" f, err := os.Open(file) @@ -164,5 +178,5 @@ func TestValidate_Extended(t *testing.T) { assert.Equal(t, file, ferr.Location.File) // location of the start of the boolean flag // which lacks a description - assert.Equal(t, 30, ferr.Location.Line) + assert.Equal(t, 33, ferr.Location.Line) } diff --git a/go.work.sum b/go.work.sum index b68df56d32..6df2a06122 100644 --- a/go.work.sum +++ b/go.work.sum @@ -670,6 +670,7 @@ github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC github.com/google/go-github/v59 v59.0.0/go.mod h1:rJU4R0rQHFVFDOkqGWxfLNo6vEk4dv40oDjhV/gH6wM= github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= @@ -1121,6 +1122,7 @@ github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -1529,6 +1531,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= diff --git a/internal/ext/common.go b/internal/ext/common.go index edfa8f04c3..075960b481 100644 --- a/internal/ext/common.go +++ b/internal/ext/common.go @@ -6,11 +6,11 @@ import ( ) type Document struct { - Version string `yaml:"version,omitempty" json:"version,omitempty"` - Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` - Flags []*Flag `yaml:"flags,omitempty" json:"flags,omitempty"` - Segments []*Segment `yaml:"segments,omitempty" json:"segments,omitempty"` - Etag string `yaml:"-" json:"-"` + Version string `yaml:"version,omitempty" json:"version,omitempty"` + Namespace *NamespaceEmbed `yaml:"namespace,omitempty" json:"namespace,omitempty"` + Flags []*Flag `yaml:"flags,omitempty" json:"flags,omitempty"` + Segments []*Segment `yaml:"segments,omitempty" json:"segments,omitempty"` + Etag string `yaml:"-" json:"-"` } type Flag struct { @@ -170,3 +170,117 @@ type Segments struct { } func (s *Segments) IsSegment() {} + +// IsNamespace is used to unify the two types of namespaces that can come in +// from the import. +type IsNamespace interface { + IsNamespace() + GetKey() string +} + +type NamespaceEmbed struct { + IsNamespace `yaml:"-"` +} + +var DefaultNamespace = &NamespaceEmbed{&Namespace{Key: "default", Name: "Default"}} + +func (n *NamespaceEmbed) String() string { + return n.IsNamespace.GetKey() +} + +// MarshalYAML tries to type assert to either of the following types that implement +// IsNamespace, and returns the marshaled value. +func (n *NamespaceEmbed) MarshalYAML() (interface{}, error) { + switch t := n.IsNamespace.(type) { + case NamespaceKey: + return string(t), nil + case *Namespace: + ns := &Namespace{ + Key: t.Key, + Name: t.Name, + Description: t.Description, + } + return ns, nil + } + + return nil, errors.New("failed to marshal to string or namespace") +} + +// UnmarshalYAML attempts to unmarshal a string or `Namespace`, and fails if it can not +// do so. +func (n *NamespaceEmbed) UnmarshalYAML(unmarshal func(interface{}) error) error { + var nk NamespaceKey + + if err := unmarshal(&nk); err == nil { + n.IsNamespace = nk + return nil + } + + var ns *Namespace + if err := unmarshal(&ns); err == nil { + n.IsNamespace = ns + return nil + } + + return errors.New("failed to unmarshal to string or namespace") +} + +// MarshalJSON tries to type assert to either of the following types that implement +// IsNamespace, and returns the marshaled value. +func (n *NamespaceEmbed) MarshalJSON() ([]byte, error) { + switch t := n.IsNamespace.(type) { + case NamespaceKey: + return json.Marshal(string(t)) + case *Namespace: + ns := &Namespace{ + Key: t.Key, + Name: t.Name, + Description: t.Description, + } + return json.Marshal(ns) + } + + return nil, errors.New("failed to marshal to string or namespace") +} + +// UnmarshalJSON attempts to unmarshal a string or `Namespace`, and fails if it can not +// do so. +func (n *NamespaceEmbed) UnmarshalJSON(v []byte) error { + var nk NamespaceKey + + if err := json.Unmarshal(v, &nk); err == nil { + n.IsNamespace = nk + return nil + } + + var ns *Namespace + if err := json.Unmarshal(v, &ns); err == nil { + n.IsNamespace = ns + return nil + } + + return errors.New("failed to unmarshal to string or namespace") +} + +type NamespaceKey string + +func (n NamespaceKey) IsNamespace() {} + +func (n NamespaceKey) GetKey() string { + return string(n) +} + +type Namespace struct { + Key string `yaml:"key,omitempty" json:"key,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` +} + +func (n *Namespace) IsNamespace() {} + +func (n *Namespace) GetKey() string { + if n == nil { + return "" + } + return n.Key +} diff --git a/internal/ext/exporter.go b/internal/ext/exporter.go index 45c5d8ad41..c6608c1e73 100644 --- a/internal/ext/exporter.go +++ b/internal/ext/exporter.go @@ -18,17 +18,20 @@ var ( v1_1 = semver.Version{Major: 1, Minor: 1} v1_2 = semver.Version{Major: 1, Minor: 2} v1_3 = semver.Version{Major: 1, Minor: 3} - latestVersion = v1_3 + v1_4 = semver.Version{Major: 1, Minor: 4} + latestVersion = v1_4 supportedVersions = semver.Versions{ v1_0, v1_1, v1_2, + v1_3, latestVersion, } ) type Lister interface { + GetNamespace(context.Context, *flipt.GetNamespaceRequest) (*flipt.Namespace, error) ListNamespaces(context.Context, *flipt.ListNamespaceRequest) (*flipt.NamespaceList, error) ListFlags(context.Context, *flipt.ListFlagRequest) (*flipt.FlagList, error) ListSegments(context.Context, *flipt.ListSegmentRequest) (*flipt.SegmentList, error) @@ -39,7 +42,7 @@ type Lister interface { type Exporter struct { store Lister batchSize int32 - namespaces []string + namespaceKeys []string allNamespaces bool } @@ -49,7 +52,7 @@ func NewExporter(store Lister, namespaces string, allNamespaces bool) *Exporter return &Exporter{ store: store, batchSize: defaultBatchSize, - namespaces: ns, + namespaceKeys: ns, allNamespaces: allNamespaces, } } @@ -67,17 +70,15 @@ func (e *Exporter) Export(ctx context.Context, encoding Encoding, w io.Writer) e defer enc.Close() - namespaces := e.namespaces + namespaces := make([]*Namespace, 0) - // If allNamespaces is "true", then retrieve all the namespaces, and store them in a string slice. + // If allNamespaces is "true", then retrieve all the namespaces, and store them in a slice. if e.allNamespaces { var ( remaining = true nextPage string ) - intermediateNamespaces := make([]string, 0) - for remaining { resp, err := e.store.ListNamespaces(ctx, &flipt.ListNamespaceRequest{ PageToken: nextPage, @@ -91,11 +92,29 @@ func (e *Exporter) Export(ctx context.Context, encoding Encoding, w io.Writer) e remaining = nextPage != "" for _, ns := range resp.Namespaces { - intermediateNamespaces = append(intermediateNamespaces, ns.Key) + namespaces = append(namespaces, &Namespace{ + Key: ns.Key, + Name: ns.Name, + Description: ns.Description, + }) } } + } else { + // If allNamespaces is "false", then retrieve the namespaces specified in the namespaceKeys slice. + for _, key := range e.namespaceKeys { + resp, err := e.store.GetNamespace(ctx, &flipt.GetNamespaceRequest{ + Key: key, + }) + if err != nil { + return fmt.Errorf("getting namespaces: %w", err) + } - namespaces = intermediateNamespaces + namespaces = append(namespaces, &Namespace{ + Key: resp.Key, + Name: resp.Name, + Description: resp.Description, + }) + } } for i := 0; i < len(namespaces); i++ { @@ -104,7 +123,10 @@ func (e *Exporter) Export(ctx context.Context, encoding Encoding, w io.Writer) e if i == 0 { doc.Version = versionString(latestVersion) } - doc.Namespace = namespaces[i] + ns := namespaces[i] + doc.Namespace = &NamespaceEmbed{ + IsNamespace: ns, + } var ( remaining = true @@ -116,7 +138,7 @@ func (e *Exporter) Export(ctx context.Context, encoding Encoding, w io.Writer) e resp, err := e.store.ListFlags( ctx, &flipt.ListFlagRequest{ - NamespaceKey: namespaces[i], + NamespaceKey: ns.Key, PageToken: nextPage, Limit: batchSize, }, @@ -171,7 +193,7 @@ func (e *Exporter) Export(ctx context.Context, encoding Encoding, w io.Writer) e resp, err := e.store.ListRules( ctx, &flipt.ListRuleRequest{ - NamespaceKey: namespaces[i], + NamespaceKey: ns.Key, FlagKey: flag.Key, }, ) @@ -210,7 +232,7 @@ func (e *Exporter) Export(ctx context.Context, encoding Encoding, w io.Writer) e } rollouts, err := e.store.ListRollouts(ctx, &flipt.ListRolloutRequest{ - NamespaceKey: namespaces[i], + NamespaceKey: ns.Key, FlagKey: flag.Key, }) if err != nil { @@ -259,7 +281,7 @@ func (e *Exporter) Export(ctx context.Context, encoding Encoding, w io.Writer) e resp, err := e.store.ListSegments( ctx, &flipt.ListSegmentRequest{ - NamespaceKey: namespaces[i], + NamespaceKey: ns.Key, PageToken: nextPage, Limit: batchSize, }, diff --git a/internal/ext/exporter_test.go b/internal/ext/exporter_test.go index 519107e27a..dcb4b78146 100644 --- a/internal/ext/exporter_test.go +++ b/internal/ext/exporter_test.go @@ -7,6 +7,8 @@ import ( "fmt" "io" "os" + "sort" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -16,7 +18,7 @@ import ( ) type mockLister struct { - namespaces []*flipt.Namespace + namespaces map[string]*flipt.Namespace nsToFlags map[string][]*flipt.Flag nsToSegments map[string][]*flipt.Segment @@ -24,9 +26,40 @@ type mockLister struct { nsToRollouts map[string][]*flipt.Rollout } +func (m mockLister) GetNamespace(_ context.Context, r *flipt.GetNamespaceRequest) (*flipt.Namespace, error) { + for k, ns := range m.namespaces { + // split the key by _ to get the namespace key, as its prefixed with an index + key := strings.Split(k, "_")[1] + if r.Key == key { + return ns, nil + } + } + + return nil, fmt.Errorf("namespace %s not found", r.Key) +} + func (m mockLister) ListNamespaces(_ context.Context, _ *flipt.ListNamespaceRequest) (*flipt.NamespaceList, error) { + var ( + keys []string + namespaces []*flipt.Namespace + ) + + // sort the namespaces by key, as they are prefixed with an index + for k := range m.namespaces { + keys = append(keys, k) + } + + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + // remove the index prefix from the key + for _, k := range keys { + namespaces = append(namespaces, m.namespaces[k]) + } + return &flipt.NamespaceList{ - Namespaces: m.namespaces, + Namespaces: namespaces, }, nil } @@ -88,6 +121,13 @@ func TestExport(t *testing.T) { { name: "single default namespace", lister: mockLister{ + namespaces: map[string]*flipt.Namespace{ + "0_default": { + Key: "default", + Name: "default", + Description: "default namespace", + }, + }, nsToFlags: map[string][]*flipt.Flag{ "default": { { @@ -233,6 +273,18 @@ func TestExport(t *testing.T) { { name: "multiple namespaces", lister: mockLister{ + namespaces: map[string]*flipt.Namespace{ + "0_default": { + Key: "default", + Name: "default", + Description: "default namespace", + }, + "1_foo": { + Key: "foo", + Name: "foo", + Description: "foo namespace", + }, + }, nsToFlags: map[string][]*flipt.Flag{ "default": { { @@ -494,15 +546,23 @@ func TestExport(t *testing.T) { { name: "all namespaces", lister: mockLister{ - namespaces: []*flipt.Namespace{ - { - Key: "default", + namespaces: map[string]*flipt.Namespace{ + "0_default": { + Key: "default", + Name: "default", + Description: "default namespace", }, - { - Key: "foo", + + "1_foo": { + Key: "foo", + Name: "foo", + Description: "foo namespace", }, - { - Key: "bar", + + "2_bar": { + Key: "bar", + Name: "bar", + Description: "bar namespace", }, }, nsToFlags: map[string][]*flipt.Flag{ diff --git a/internal/ext/importer.go b/internal/ext/importer.go index c26fc3cd9d..673e28f309 100644 --- a/internal/ext/importer.go +++ b/internal/ext/importer.go @@ -87,20 +87,35 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx } } - namespace := doc.Namespace + var namespaceKey = flipt.DefaultNamespace - if namespace != "" && namespace != flipt.DefaultNamespace { + // non-default namespace, create it if it doesn't exist + if doc.Namespace != nil && doc.Namespace.GetKey() != flipt.DefaultNamespace { + namespaceKey = doc.Namespace.GetKey() _, err := i.creator.GetNamespace(ctx, &flipt.GetNamespaceRequest{ - Key: namespace, + Key: namespaceKey, }) if err != nil { if status.Code(err) != codes.NotFound && !errs.AsMatch[errs.ErrNotFound](err) { return err } + var ( + namespaceName, namespaceDescription string + ) + + switch ns := doc.Namespace.IsNamespace.(type) { + case NamespaceKey: + namespaceName = string(ns) + case *Namespace: + namespaceName = ns.Name + namespaceDescription = ns.Description + } + _, err = i.creator.CreateNamespace(ctx, &flipt.CreateNamespaceRequest{ - Key: namespace, - Name: namespace, + Key: namespaceKey, + Name: namespaceName, + Description: namespaceDescription, }) if err != nil { return err @@ -122,12 +137,12 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx ) if skipExisting { - existingFlags, err = i.existingFlags(ctx, namespace) + existingFlags, err = i.existingFlags(ctx, namespaceKey) if err != nil { return err } - existingSegments, err = i.existingSegments(ctx, namespace) + existingSegments, err = i.existingSegments(ctx, namespaceKey) if err != nil { return err } @@ -146,7 +161,7 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx Name: f.Name, Description: f.Description, Enabled: f.Enabled, - NamespaceKey: namespace, + NamespaceKey: namespaceKey, } if f.Metadata != nil { @@ -194,7 +209,7 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx Name: v.Name, Description: v.Description, Attachment: string(out), - NamespaceKey: namespace, + NamespaceKey: namespaceKey, }) if err != nil { return fmt.Errorf("creating variant: %w", err) @@ -218,7 +233,7 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx Name: flag.Name, Description: flag.Description, Enabled: flag.Enabled, - NamespaceKey: namespace, + NamespaceKey: namespaceKey, DefaultVariantId: defaultVariantId, }) @@ -243,7 +258,7 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx Name: s.Name, Description: s.Description, MatchType: flipt.MatchType(flipt.MatchType_value[s.MatchType]), - NamespaceKey: namespace, + NamespaceKey: namespaceKey, }) if err != nil { return fmt.Errorf("creating segment: %w", err) @@ -260,7 +275,7 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx Property: c.Property, Operator: c.Operator, Value: c.Value, - NamespaceKey: namespace, + NamespaceKey: namespaceKey, }) if err != nil { return fmt.Errorf("creating constraint: %w", err) @@ -293,7 +308,7 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx fcr := &flipt.CreateRuleRequest{ FlagKey: f.Key, Rank: rank, - NamespaceKey: namespace, + NamespaceKey: namespaceKey, } switch s := r.Segment.IsSegment.(type) { @@ -324,7 +339,7 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx RuleId: rule.Id, VariantId: variant.Id, Rollout: d.Rollout, - NamespaceKey: namespace, + NamespaceKey: namespaceKey, }) if err != nil { return fmt.Errorf("creating distribution: %w", err) @@ -341,14 +356,14 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx for idx, r := range f.Rollouts { if r.Segment != nil && r.Threshold != nil { return fmt.Errorf(`rollout "%s/%s/%d" cannot have both segment and percentage rule`, - namespace, + namespaceKey, f.Key, idx, ) } req := &flipt.CreateRolloutRequest{ - NamespaceKey: namespace, + NamespaceKey: namespaceKey, FlagKey: f.Key, Description: r.Description, Rank: int32(idx + 1), @@ -362,7 +377,7 @@ func (i *Importer) Import(ctx context.Context, enc Encoding, r io.Reader, skipEx if len(r.Segment.Keys) > 0 && r.Segment.Key != "" { return fmt.Errorf("rollout %s/%s/%d cannot have both segment.keys and segment.key", - namespace, + namespaceKey, f.Key, idx, ) diff --git a/internal/ext/importer_test.go b/internal/ext/importer_test.go index f2ebaf8f6a..fab92edbf4 100644 --- a/internal/ext/importer_test.go +++ b/internal/ext/importer_test.go @@ -74,8 +74,8 @@ func (m *mockCreator) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest return nil, m.createflagErr } return &flipt.Flag{ - NamespaceKey: r.NamespaceKey, Key: r.Key, + NamespaceKey: r.NamespaceKey, Name: r.Name, Description: r.Description, Type: r.Type, @@ -90,8 +90,8 @@ func (m *mockCreator) UpdateFlag(ctx context.Context, r *flipt.UpdateFlagRequest return nil, m.updateFlagErr } return &flipt.Flag{ - NamespaceKey: r.NamespaceKey, Key: r.Key, + NamespaceKey: r.NamespaceKey, Name: r.Name, Description: r.Description, DefaultVariant: &flipt.Variant{ @@ -107,12 +107,13 @@ func (m *mockCreator) CreateVariant(ctx context.Context, r *flipt.CreateVariantR return nil, m.variantErr } return &flipt.Variant{ - Id: "static_variant_id", - FlagKey: r.FlagKey, - Key: r.Key, - Name: r.Name, - Description: r.Description, - Attachment: r.Attachment, + Id: "static_variant_id", + NamespaceKey: r.NamespaceKey, + FlagKey: r.FlagKey, + Key: r.Key, + Name: r.Name, + Description: r.Description, + Attachment: r.Attachment, }, nil } @@ -122,10 +123,11 @@ func (m *mockCreator) CreateSegment(ctx context.Context, r *flipt.CreateSegmentR return nil, m.segmentErr } return &flipt.Segment{ - Key: r.Key, - Name: r.Name, - Description: r.Description, - MatchType: r.MatchType, + Key: r.Key, + NamespaceKey: r.NamespaceKey, + Name: r.Name, + Description: r.Description, + MatchType: r.MatchType, }, nil } @@ -135,12 +137,13 @@ func (m *mockCreator) CreateConstraint(ctx context.Context, r *flipt.CreateConst return nil, m.constraintErr } return &flipt.Constraint{ - Id: "static_constraint_id", - SegmentKey: r.SegmentKey, - Type: r.Type, - Property: r.Property, - Operator: r.Operator, - Value: r.Value, + Id: "static_constraint_id", + NamespaceKey: r.NamespaceKey, + SegmentKey: r.SegmentKey, + Type: r.Type, + Property: r.Property, + Operator: r.Operator, + Value: r.Value, }, nil } @@ -150,10 +153,11 @@ func (m *mockCreator) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest return nil, m.ruleErr } return &flipt.Rule{ - Id: "static_rule_id", - FlagKey: r.FlagKey, - SegmentKey: r.SegmentKey, - Rank: r.Rank, + Id: "static_rule_id", + NamespaceKey: r.NamespaceKey, + FlagKey: r.FlagKey, + SegmentKey: r.SegmentKey, + Rank: r.Rank, }, nil } @@ -258,31 +262,35 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, }, { - Key: "flag2", - Name: "flag2", - Description: "a boolean flag", - Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, - Enabled: false, + NamespaceKey: "default", + Key: "flag2", + Name: "flag2", + Description: "a boolean flag", + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + Enabled: false, }, }, variantReqs: []*flipt.CreateVariantRequest{ { - FlagKey: "flag1", - Key: "variant1", - Name: "variant1", - Description: "variant description", - Attachment: compact(t, variantAttachment), + NamespaceKey: "default", + FlagKey: "flag1", + Key: "variant1", + Name: "variant1", + Description: "variant description", + Attachment: compact(t, variantAttachment), }, }, updateFlagReqs: []*flipt.UpdateFlagRequest{ { + NamespaceKey: "default", Key: "flag1", Name: "flag1", Description: "description", @@ -292,41 +300,46 @@ func TestImport(t *testing.T) { }, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment1", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "fizz", - Operator: "neq", - Value: "buzz", + NamespaceKey: "default", + SegmentKey: "segment1", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "fizz", + Operator: "neq", + Value: "buzz", }, }, ruleReqs: []*flipt.CreateRuleRequest{ { - FlagKey: "flag1", - SegmentKey: "segment1", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag1", + SegmentKey: "segment1", + Rank: 1, }, }, distributionReqs: []*flipt.CreateDistributionRequest{ { - RuleId: "static_rule_id", - VariantId: "static_variant_id", - FlagKey: "flag1", - Rollout: 100, + NamespaceKey: "default", + RuleId: "static_rule_id", + VariantId: "static_variant_id", + FlagKey: "flag1", + Rollout: 100, }, }, rolloutReqs: []*flipt.CreateRolloutRequest{ { - FlagKey: "flag2", - Description: "enabled for internal users", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for internal users", + Rank: 1, Rule: &flipt.CreateRolloutRequest_Segment{ Segment: &flipt.RolloutSegment{ SegmentKey: "internal_users", @@ -335,9 +348,10 @@ func TestImport(t *testing.T) { }, }, { - FlagKey: "flag2", - Description: "enabled for 50%", - Rank: 2, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for 50%", + Rank: 2, Rule: &flipt.CreateRolloutRequest_Threshold{ Threshold: &flipt.RolloutThreshold{ Percentage: 50.0, @@ -354,66 +368,74 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, }, { - Key: "flag2", - Name: "flag2", - Description: "a boolean flag", - Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, - Enabled: false, + NamespaceKey: "default", + Key: "flag2", + Name: "flag2", + Description: "a boolean flag", + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + Enabled: false, }, }, variantReqs: []*flipt.CreateVariantRequest{ { - FlagKey: "flag1", - Key: "variant1", - Name: "variant1", - Description: "variant description", - Attachment: compact(t, variantAttachment), + NamespaceKey: "default", + FlagKey: "flag1", + Key: "variant1", + Name: "variant1", + Description: "variant description", + Attachment: compact(t, variantAttachment), }, }, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment1", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "fizz", - Operator: "neq", - Value: "buzz", + NamespaceKey: "default", + SegmentKey: "segment1", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "fizz", + Operator: "neq", + Value: "buzz", }, }, ruleReqs: []*flipt.CreateRuleRequest{ { - FlagKey: "flag1", - SegmentKey: "segment1", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag1", + SegmentKey: "segment1", + Rank: 1, }, }, distributionReqs: []*flipt.CreateDistributionRequest{ { - RuleId: "static_rule_id", - VariantId: "static_variant_id", - FlagKey: "flag1", - Rollout: 100, + NamespaceKey: "default", + RuleId: "static_rule_id", + VariantId: "static_variant_id", + FlagKey: "flag1", + Rollout: 100, }, }, rolloutReqs: []*flipt.CreateRolloutRequest{ { - FlagKey: "flag2", - Description: "enabled for internal users", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for internal users", + Rank: 1, Rule: &flipt.CreateRolloutRequest_Segment{ Segment: &flipt.RolloutSegment{ SegmentKey: "internal_users", @@ -422,9 +444,10 @@ func TestImport(t *testing.T) { }, }, { - FlagKey: "flag2", - Description: "enabled for 50%", - Rank: 2, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for 50%", + Rank: 2, Rule: &flipt.CreateRolloutRequest_Threshold{ Threshold: &flipt.RolloutThreshold{ Percentage: 50.0, @@ -441,65 +464,73 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, }, { - Key: "flag2", - Name: "flag2", - Description: "a boolean flag", - Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, - Enabled: false, + NamespaceKey: "default", + Key: "flag2", + Name: "flag2", + Description: "a boolean flag", + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + Enabled: false, }, }, variantReqs: []*flipt.CreateVariantRequest{ { - FlagKey: "flag1", - Key: "variant1", - Name: "variant1", - Description: "variant description", + NamespaceKey: "default", + FlagKey: "flag1", + Key: "variant1", + Name: "variant1", + Description: "variant description", }, }, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment1", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "fizz", - Operator: "neq", - Value: "buzz", + NamespaceKey: "default", + SegmentKey: "segment1", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "fizz", + Operator: "neq", + Value: "buzz", }, }, ruleReqs: []*flipt.CreateRuleRequest{ { - FlagKey: "flag1", - SegmentKey: "segment1", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag1", + SegmentKey: "segment1", + Rank: 1, }, }, distributionReqs: []*flipt.CreateDistributionRequest{ { - RuleId: "static_rule_id", - VariantId: "static_variant_id", - FlagKey: "flag1", - Rollout: 100, + NamespaceKey: "default", + RuleId: "static_rule_id", + VariantId: "static_variant_id", + FlagKey: "flag1", + Rollout: 100, }, }, rolloutReqs: []*flipt.CreateRolloutRequest{ { - FlagKey: "flag2", - Description: "enabled for internal users", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for internal users", + Rank: 1, Rule: &flipt.CreateRolloutRequest_Segment{ Segment: &flipt.RolloutSegment{ SegmentKey: "internal_users", @@ -508,9 +539,10 @@ func TestImport(t *testing.T) { }, }, { - FlagKey: "flag2", - Description: "enabled for 50%", - Rank: 2, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for 50%", + Rank: 2, Rule: &flipt.CreateRolloutRequest_Threshold{ Threshold: &flipt.RolloutThreshold{ Percentage: 50.0, @@ -527,66 +559,74 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, }, { - Key: "flag2", - Name: "flag2", - Description: "a boolean flag", - Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, - Enabled: false, + NamespaceKey: "default", + Key: "flag2", + Name: "flag2", + Description: "a boolean flag", + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + Enabled: false, }, }, variantReqs: []*flipt.CreateVariantRequest{ { - FlagKey: "flag1", - Key: "variant1", - Name: "variant1", - Description: "variant description", - Attachment: compact(t, variantAttachment), + NamespaceKey: "default", + FlagKey: "flag1", + Key: "variant1", + Name: "variant1", + Description: "variant description", + Attachment: compact(t, variantAttachment), }, }, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment1", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "fizz", - Operator: "neq", - Value: "buzz", + NamespaceKey: "default", + SegmentKey: "segment1", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "fizz", + Operator: "neq", + Value: "buzz", }, }, ruleReqs: []*flipt.CreateRuleRequest{ { - FlagKey: "flag1", - SegmentKey: "segment1", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag1", + SegmentKey: "segment1", + Rank: 1, }, }, distributionReqs: []*flipt.CreateDistributionRequest{ { - RuleId: "static_rule_id", - VariantId: "static_variant_id", - FlagKey: "flag1", - Rollout: 100, + NamespaceKey: "default", + RuleId: "static_rule_id", + VariantId: "static_variant_id", + FlagKey: "flag1", + Rollout: 100, }, }, rolloutReqs: []*flipt.CreateRolloutRequest{ { - FlagKey: "flag2", - Description: "enabled for internal users", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for internal users", + Rank: 1, Rule: &flipt.CreateRolloutRequest_Segment{ Segment: &flipt.RolloutSegment{ SegmentKey: "internal_users", @@ -595,9 +635,10 @@ func TestImport(t *testing.T) { }, }, { - FlagKey: "flag2", - Description: "enabled for 50%", - Rank: 2, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for 50%", + Rank: 2, Rule: &flipt.CreateRolloutRequest_Threshold{ Threshold: &flipt.RolloutThreshold{ Percentage: 50.0, @@ -614,66 +655,74 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, }, { - Key: "flag2", - Name: "flag2", - Description: "a boolean flag", - Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, - Enabled: false, + NamespaceKey: "default", + Key: "flag2", + Name: "flag2", + Description: "a boolean flag", + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + Enabled: false, }, }, variantReqs: []*flipt.CreateVariantRequest{ { - FlagKey: "flag1", - Key: "variant1", - Name: "variant1", - Description: "variant description", - Attachment: compact(t, variantAttachment), + NamespaceKey: "default", + FlagKey: "flag1", + Key: "variant1", + Name: "variant1", + Description: "variant description", + Attachment: compact(t, variantAttachment), }, }, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment1", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "fizz", - Operator: "neq", - Value: "buzz", + NamespaceKey: "default", + SegmentKey: "segment1", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "fizz", + Operator: "neq", + Value: "buzz", }, }, ruleReqs: []*flipt.CreateRuleRequest{ { - FlagKey: "flag1", - SegmentKeys: []string{"segment1"}, - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag1", + SegmentKeys: []string{"segment1"}, + Rank: 1, }, }, distributionReqs: []*flipt.CreateDistributionRequest{ { - RuleId: "static_rule_id", - VariantId: "static_variant_id", - FlagKey: "flag1", - Rollout: 100, + NamespaceKey: "default", + RuleId: "static_rule_id", + VariantId: "static_variant_id", + FlagKey: "flag1", + Rollout: 100, }, }, rolloutReqs: []*flipt.CreateRolloutRequest{ { - FlagKey: "flag2", - Description: "enabled for internal users", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for internal users", + Rank: 1, Rule: &flipt.CreateRolloutRequest_Segment{ Segment: &flipt.RolloutSegment{ SegmentKey: "internal_users", @@ -682,9 +731,10 @@ func TestImport(t *testing.T) { }, }, { - FlagKey: "flag2", - Description: "enabled for 50%", - Rank: 2, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for 50%", + Rank: 2, Rule: &flipt.CreateRolloutRequest_Threshold{ Threshold: &flipt.RolloutThreshold{ Percentage: 50.0, @@ -701,52 +751,58 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, }, }, variantReqs: []*flipt.CreateVariantRequest{ { - FlagKey: "flag1", - Key: "variant1", - Name: "variant1", - Description: "variant description", - Attachment: compact(t, variantAttachment), + NamespaceKey: "default", + FlagKey: "flag1", + Key: "variant1", + Name: "variant1", + Description: "variant description", + Attachment: compact(t, variantAttachment), }, }, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment1", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "fizz", - Operator: "neq", - Value: "buzz", + NamespaceKey: "default", + SegmentKey: "segment1", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "fizz", + Operator: "neq", + Value: "buzz", }, }, ruleReqs: []*flipt.CreateRuleRequest{ { - FlagKey: "flag1", - SegmentKey: "segment1", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag1", + SegmentKey: "segment1", + Rank: 1, }, }, distributionReqs: []*flipt.CreateDistributionRequest{ { - RuleId: "static_rule_id", - VariantId: "static_variant_id", - FlagKey: "flag1", - Rollout: 100, + NamespaceKey: "default", + RuleId: "static_rule_id", + VariantId: "static_variant_id", + FlagKey: "flag1", + Rollout: 100, }, }, }, @@ -757,66 +813,74 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, }, { - Key: "flag2", - Name: "flag2", - Description: "a boolean flag", - Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, - Enabled: false, + NamespaceKey: "default", + Key: "flag2", + Name: "flag2", + Description: "a boolean flag", + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + Enabled: false, }, }, variantReqs: []*flipt.CreateVariantRequest{ { - FlagKey: "flag1", - Key: "variant1", - Name: "variant1", - Description: "variant description", - Attachment: compact(t, variantAttachment), + NamespaceKey: "default", + FlagKey: "flag1", + Key: "variant1", + Name: "variant1", + Description: "variant description", + Attachment: compact(t, variantAttachment), }, }, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment1", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "fizz", - Operator: "neq", - Value: "buzz", + NamespaceKey: "default", + SegmentKey: "segment1", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "fizz", + Operator: "neq", + Value: "buzz", }, }, ruleReqs: []*flipt.CreateRuleRequest{ { - FlagKey: "flag1", - SegmentKey: "segment1", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag1", + SegmentKey: "segment1", + Rank: 1, }, }, distributionReqs: []*flipt.CreateDistributionRequest{ { - RuleId: "static_rule_id", - VariantId: "static_variant_id", - FlagKey: "flag1", - Rollout: 100, + NamespaceKey: "default", + RuleId: "static_rule_id", + VariantId: "static_variant_id", + FlagKey: "flag1", + Rollout: 100, }, }, rolloutReqs: []*flipt.CreateRolloutRequest{ { - FlagKey: "flag2", - Description: "enabled for internal users", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for internal users", + Rank: 1, Rule: &flipt.CreateRolloutRequest_Segment{ Segment: &flipt.RolloutSegment{ SegmentKey: "internal_users", @@ -825,9 +889,10 @@ func TestImport(t *testing.T) { }, }, { - FlagKey: "flag2", - Description: "enabled for 50%", - Rank: 2, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for 50%", + Rank: 2, Rule: &flipt.CreateRolloutRequest_Threshold{ Threshold: &flipt.RolloutThreshold{ Percentage: 50.0, @@ -846,19 +911,21 @@ func TestImport(t *testing.T) { return &mockCreator{ listFlagResps: []*flipt.FlagList{{ Flags: []*flipt.Flag{{ - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, }}, }}, listSegmentResps: []*flipt.SegmentList{{ Segments: []*flipt.Segment{{ - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }}, }}, } @@ -866,38 +933,42 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag2", - Name: "flag2", - Description: "a boolean flag", - Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, - Enabled: false, + NamespaceKey: "default", + Key: "flag2", + Name: "flag2", + Description: "a boolean flag", + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + Enabled: false, }, }, variantReqs: nil, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment2", - Name: "segment2", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment2", + Name: "segment2", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "buzz", - Operator: "neq", - Value: "fizz", + NamespaceKey: "default", + SegmentKey: "segment2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "buzz", + Operator: "neq", + Value: "fizz", }, }, ruleReqs: nil, distributionReqs: nil, rolloutReqs: []*flipt.CreateRolloutRequest{ { - FlagKey: "flag2", - Description: "enabled for internal users", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for internal users", + Rank: 1, Rule: &flipt.CreateRolloutRequest_Segment{ Segment: &flipt.RolloutSegment{ SegmentKey: "internal_users", @@ -906,9 +977,10 @@ func TestImport(t *testing.T) { }, }, { - FlagKey: "flag2", - Description: "enabled for 50%", - Rank: 2, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for 50%", + Rank: 2, Rule: &flipt.CreateRolloutRequest_Threshold{ Threshold: &flipt.RolloutThreshold{ Percentage: 50.0, @@ -918,11 +990,15 @@ func TestImport(t *testing.T) { }, }, listFlagReqs: []*flipt.ListFlagRequest{ - {}, + { + NamespaceKey: "default", + }, }, listFlagResps: []*flipt.FlagList{}, listSegmentReqs: []*flipt.ListSegmentRequest{ - {}, + { + NamespaceKey: "default", + }, }, listSegmentResps: []*flipt.SegmentList{}, }, @@ -933,68 +1009,76 @@ func TestImport(t *testing.T) { expected: &mockCreator{ createflagReqs: []*flipt.CreateFlagRequest{ { - Key: "flag1", - Name: "flag1", - Description: "description", - Type: flipt.FlagType_VARIANT_FLAG_TYPE, - Enabled: true, - Metadata: newStruct(t, map[string]any{"label": "variant", "area": true}), + NamespaceKey: "default", + Key: "flag1", + Name: "flag1", + Description: "description", + Type: flipt.FlagType_VARIANT_FLAG_TYPE, + Enabled: true, + Metadata: newStruct(t, map[string]any{"label": "variant", "area": true}), }, { - Key: "flag2", - Name: "flag2", - Description: "a boolean flag", - Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, - Enabled: false, - Metadata: newStruct(t, map[string]any{"label": "bool", "area": 12}), + NamespaceKey: "default", + Key: "flag2", + Name: "flag2", + Description: "a boolean flag", + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + Enabled: false, + Metadata: newStruct(t, map[string]any{"label": "bool", "area": 12}), }, }, variantReqs: []*flipt.CreateVariantRequest{ { - FlagKey: "flag1", - Key: "variant1", - Name: "variant1", - Description: "variant description", - Attachment: compact(t, variantAttachment), + NamespaceKey: "default", + FlagKey: "flag1", + Key: "variant1", + Name: "variant1", + Description: "variant description", + Attachment: compact(t, variantAttachment), }, }, segmentReqs: []*flipt.CreateSegmentRequest{ { - Key: "segment1", - Name: "segment1", - Description: "description", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, + NamespaceKey: "default", + Key: "segment1", + Name: "segment1", + Description: "description", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, }, }, constraintReqs: []*flipt.CreateConstraintRequest{ { - SegmentKey: "segment1", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "fizz", - Operator: "neq", - Value: "buzz", + NamespaceKey: "default", + SegmentKey: "segment1", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "fizz", + Operator: "neq", + Value: "buzz", }, }, ruleReqs: []*flipt.CreateRuleRequest{ { - FlagKey: "flag1", - SegmentKey: "segment1", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag1", + SegmentKey: "segment1", + Rank: 1, }, }, distributionReqs: []*flipt.CreateDistributionRequest{ { - RuleId: "static_rule_id", - VariantId: "static_variant_id", - FlagKey: "flag1", - Rollout: 100, + NamespaceKey: "default", + RuleId: "static_rule_id", + VariantId: "static_variant_id", + FlagKey: "flag1", + Rollout: 100, }, }, rolloutReqs: []*flipt.CreateRolloutRequest{ { - FlagKey: "flag2", - Description: "enabled for internal users", - Rank: 1, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for internal users", + Rank: 1, Rule: &flipt.CreateRolloutRequest_Segment{ Segment: &flipt.RolloutSegment{ SegmentKey: "internal_users", @@ -1003,9 +1087,10 @@ func TestImport(t *testing.T) { }, }, { - FlagKey: "flag2", - Description: "enabled for 50%", - Rank: 2, + NamespaceKey: "default", + FlagKey: "flag2", + Description: "enabled for 50%", + Rank: 2, Rule: &flipt.CreateRolloutRequest_Threshold{ Threshold: &flipt.RolloutThreshold{ Percentage: 50.0, diff --git a/internal/ext/testdata/export.json b/internal/ext/testdata/export.json index 194a8bd3df..2d66b815fd 100644 --- a/internal/ext/testdata/export.json +++ b/internal/ext/testdata/export.json @@ -1,6 +1,10 @@ { - "version": "1.3", - "namespace": "default", + "version": "1.4", + "namespace": { + "key": "default", + "name": "default", + "description": "default namespace" + }, "flags": [ { "key": "flag1", diff --git a/internal/ext/testdata/export.yml b/internal/ext/testdata/export.yml index f03a6c77f0..9360967584 100644 --- a/internal/ext/testdata/export.yml +++ b/internal/ext/testdata/export.yml @@ -1,5 +1,8 @@ -version: "1.3" -namespace: default +version: "1.4" +namespace: + key: default + name: default + description: default namespace flags: - key: flag1 name: flag1 diff --git a/internal/ext/testdata/export_all_namespaces.json b/internal/ext/testdata/export_all_namespaces.json index 3611adc380..ac42acd432 100644 --- a/internal/ext/testdata/export_all_namespaces.json +++ b/internal/ext/testdata/export_all_namespaces.json @@ -1,3 +1,3 @@ -{"version":"1.3","namespace":"default"} -{"namespace":"foo","flags":[{"key":"flag1","name":"flag1","type":"VARIANT_FLAG_TYPE","description":"description","enabled":true,"variants":[{"key":"variant1","name":"variant1","attachment":{"pi":3.141,"happy":true,"name":"Niels","nothing":null,"answer":{"everything":42},"list":[1,0,2],"object":{"currency":"USD","value":42.99}}},{"key":"foo"}],"rules":[{"segment":"segment1","distributions":[{"variant":"variant1","rollout":100}]},{"segment":{"keys":["segment1","segment2"],"operator":"AND_SEGMENT_OPERATOR"}}]},{"key":"flag2","name":"flag2","type":"BOOLEAN_FLAG_TYPE","description":"a boolean flag","enabled":false,"rollouts":[{"description":"enabled for internal users","segment":{"key":"internal_users","value":true}},{"description":"enabled for 50%","threshold":{"percentage":50,"value":true}}]}],"segments":[{"key":"segment1","name":"segment1","match_type":"ANY_MATCH_TYPE","description":"description","constraints":[{"type":"STRING_COMPARISON_TYPE","property":"foo","operator":"eq","value":"baz","description":"desc"},{"type":"STRING_COMPARISON_TYPE","property":"fizz","operator":"neq","value":"buzz","description":"desc"}]},{"key":"segment2","name":"segment2","match_type":"ANY_MATCH_TYPE","description":"description"}]} -{"namespace":"bar","flags":[{"key":"flag1","name":"flag1","type":"VARIANT_FLAG_TYPE","description":"description","enabled":true,"variants":[{"key":"variant1","name":"variant1","attachment":{"pi":3.141,"happy":true,"name":"Niels","nothing":null,"answer":{"everything":42},"list":[1,0,2],"object":{"currency":"USD","value":42.99}}},{"key":"foo"}],"rules":[{"segment":"segment1","distributions":[{"variant":"variant1","rollout":100}]},{"segment":{"keys":["segment1","segment2"],"operator":"AND_SEGMENT_OPERATOR"}}]},{"key":"flag2","name":"flag2","type":"BOOLEAN_FLAG_TYPE","description":"a boolean flag","enabled":false,"rollouts":[{"description":"enabled for internal users","segment":{"key":"internal_users","value":true}},{"description":"enabled for 50%","threshold":{"percentage":50,"value":true}}]}],"segments":[{"key":"segment1","name":"segment1","match_type":"ANY_MATCH_TYPE","description":"description","constraints":[{"type":"STRING_COMPARISON_TYPE","property":"foo","operator":"eq","value":"baz","description":"desc"},{"type":"STRING_COMPARISON_TYPE","property":"fizz","operator":"neq","value":"buzz","description":"desc"}]},{"key":"segment2","name":"segment2","match_type":"ANY_MATCH_TYPE","description":"description"}]} +{"version":"1.4","namespace":{"key":"default","name": "default","description":"default namespace"}} +{"namespace":{"key":"foo","name":"foo","description":"foo namespace"},"flags":[{"key":"flag1","name":"flag1","type":"VARIANT_FLAG_TYPE","description":"description","enabled":true,"variants":[{"key":"variant1","name":"variant1","attachment":{"pi":3.141,"happy":true,"name":"Niels","nothing":null,"answer":{"everything":42},"list":[1,0,2],"object":{"currency":"USD","value":42.99}}},{"key":"foo"}],"rules":[{"segment":"segment1","distributions":[{"variant":"variant1","rollout":100}]},{"segment":{"keys":["segment1","segment2"],"operator":"AND_SEGMENT_OPERATOR"}}]},{"key":"flag2","name":"flag2","type":"BOOLEAN_FLAG_TYPE","description":"a boolean flag","enabled":false,"rollouts":[{"description":"enabled for internal users","segment":{"key":"internal_users","value":true}},{"description":"enabled for 50%","threshold":{"percentage":50,"value":true}}]}],"segments":[{"key":"segment1","name":"segment1","match_type":"ANY_MATCH_TYPE","description":"description","constraints":[{"type":"STRING_COMPARISON_TYPE","property":"foo","operator":"eq","value":"baz","description":"desc"},{"type":"STRING_COMPARISON_TYPE","property":"fizz","operator":"neq","value":"buzz","description":"desc"}]},{"key":"segment2","name":"segment2","match_type":"ANY_MATCH_TYPE","description":"description"}]} +{"namespace":{"key":"bar","name":"bar","description":"bar namespace"},"flags":[{"key":"flag1","name":"flag1","type":"VARIANT_FLAG_TYPE","description":"description","enabled":true,"variants":[{"key":"variant1","name":"variant1","attachment":{"pi":3.141,"happy":true,"name":"Niels","nothing":null,"answer":{"everything":42},"list":[1,0,2],"object":{"currency":"USD","value":42.99}}},{"key":"foo"}],"rules":[{"segment":"segment1","distributions":[{"variant":"variant1","rollout":100}]},{"segment":{"keys":["segment1","segment2"],"operator":"AND_SEGMENT_OPERATOR"}}]},{"key":"flag2","name":"flag2","type":"BOOLEAN_FLAG_TYPE","description":"a boolean flag","enabled":false,"rollouts":[{"description":"enabled for internal users","segment":{"key":"internal_users","value":true}},{"description":"enabled for 50%","threshold":{"percentage":50,"value":true}}]}],"segments":[{"key":"segment1","name":"segment1","match_type":"ANY_MATCH_TYPE","description":"description","constraints":[{"type":"STRING_COMPARISON_TYPE","property":"foo","operator":"eq","value":"baz","description":"desc"},{"type":"STRING_COMPARISON_TYPE","property":"fizz","operator":"neq","value":"buzz","description":"desc"}]},{"key":"segment2","name":"segment2","match_type":"ANY_MATCH_TYPE","description":"description"}]} diff --git a/internal/ext/testdata/export_all_namespaces.yml b/internal/ext/testdata/export_all_namespaces.yml index 6ee1608dda..4a74471cc7 100644 --- a/internal/ext/testdata/export_all_namespaces.yml +++ b/internal/ext/testdata/export_all_namespaces.yml @@ -1,7 +1,13 @@ -version: "1.3" -namespace: default +version: "1.4" +namespace: + key: default + name: default + description: default namespace --- -namespace: foo +namespace: + key: foo + name: foo + description: foo namespace flags: - key: flag1 name: flag1 @@ -71,7 +77,10 @@ segments: match_type: "ANY_MATCH_TYPE" description: description --- -namespace: bar +namespace: + key: bar + name: bar + description: bar namespace flags: - key: flag1 name: flag1 diff --git a/internal/ext/testdata/export_default_and_foo.json b/internal/ext/testdata/export_default_and_foo.json index 117a87d5de..2922e56114 100644 --- a/internal/ext/testdata/export_default_and_foo.json +++ b/internal/ext/testdata/export_default_and_foo.json @@ -1,2 +1,2 @@ -{"version":"1.3","namespace":"default","flags":[{"key":"flag1","name":"flag1","type":"VARIANT_FLAG_TYPE","description":"description","enabled":true,"variants":[{"key":"variant1","name":"variant1","attachment":{"pi":3.141,"happy":true,"name":"Niels","nothing":null,"answer":{"everything":42},"list":[1,0,2],"object":{"currency":"USD","value":42.99}}},{"key":"foo"}],"rules":[{"segment":"segment1","distributions":[{"variant":"variant1","rollout":100}]},{"segment":{"keys":["segment1","segment2"],"operator":"AND_SEGMENT_OPERATOR"}}]},{"key":"flag2","name":"flag2","type":"BOOLEAN_FLAG_TYPE","description":"a boolean flag","enabled":false,"rollouts":[{"description":"enabled for internal users","segment":{"key":"internal_users","value":true}},{"description":"enabled for 50%","threshold":{"percentage":50,"value":true}}]}],"segments":[{"key":"segment1","name":"segment1","match_type":"ANY_MATCH_TYPE","description":"description","constraints":[{"type":"STRING_COMPARISON_TYPE","property":"foo","operator":"eq","value":"baz","description":"desc"},{"type":"STRING_COMPARISON_TYPE","property":"fizz","operator":"neq","value":"buzz","description":"desc"}]},{"key":"segment2","name":"segment2","match_type":"ANY_MATCH_TYPE","description":"description"}]} -{"namespace":"foo","flags":[{"key":"flag1","name":"flag1","type":"VARIANT_FLAG_TYPE","description":"description","enabled":true,"variants":[{"key":"variant1","name":"variant1","attachment":{"pi":3.141,"happy":true,"name":"Niels","nothing":null,"answer":{"everything":42},"list":[1,0,2],"object":{"currency":"USD","value":42.99}}},{"key":"foo"}],"rules":[{"segment":"segment1","distributions":[{"variant":"variant1","rollout":100}]},{"segment":{"keys":["segment1","segment2"],"operator":"AND_SEGMENT_OPERATOR"}}]},{"key":"flag2","name":"flag2","type":"BOOLEAN_FLAG_TYPE","description":"a boolean flag","enabled":false,"rollouts":[{"description":"enabled for internal users","segment":{"key":"internal_users","value":true}},{"description":"enabled for 50%","threshold":{"percentage":50,"value":true}}]}],"segments":[{"key":"segment1","name":"segment1","match_type":"ANY_MATCH_TYPE","description":"description","constraints":[{"type":"STRING_COMPARISON_TYPE","property":"foo","operator":"eq","value":"baz","description":"desc"},{"type":"STRING_COMPARISON_TYPE","property":"fizz","operator":"neq","value":"buzz","description":"desc"}]},{"key":"segment2","name":"segment2","match_type":"ANY_MATCH_TYPE","description":"description"}]} +{"version":"1.4","namespace":{"key":"default","name":"default","description":"default namespace"},"flags":[{"key":"flag1","name":"flag1","type":"VARIANT_FLAG_TYPE","description":"description","enabled":true,"variants":[{"key":"variant1","name":"variant1","attachment":{"pi":3.141,"happy":true,"name":"Niels","nothing":null,"answer":{"everything":42},"list":[1,0,2],"object":{"currency":"USD","value":42.99}}},{"key":"foo"}],"rules":[{"segment":"segment1","distributions":[{"variant":"variant1","rollout":100}]},{"segment":{"keys":["segment1","segment2"],"operator":"AND_SEGMENT_OPERATOR"}}]},{"key":"flag2","name":"flag2","type":"BOOLEAN_FLAG_TYPE","description":"a boolean flag","enabled":false,"rollouts":[{"description":"enabled for internal users","segment":{"key":"internal_users","value":true}},{"description":"enabled for 50%","threshold":{"percentage":50,"value":true}}]}],"segments":[{"key":"segment1","name":"segment1","match_type":"ANY_MATCH_TYPE","description":"description","constraints":[{"type":"STRING_COMPARISON_TYPE","property":"foo","operator":"eq","value":"baz","description":"desc"},{"type":"STRING_COMPARISON_TYPE","property":"fizz","operator":"neq","value":"buzz","description":"desc"}]},{"key":"segment2","name":"segment2","match_type":"ANY_MATCH_TYPE","description":"description"}]} +{"namespace":{"key":"foo","name":"foo","description":"foo namespace"},"flags":[{"key":"flag1","name":"flag1","type":"VARIANT_FLAG_TYPE","description":"description","enabled":true,"variants":[{"key":"variant1","name":"variant1","attachment":{"pi":3.141,"happy":true,"name":"Niels","nothing":null,"answer":{"everything":42},"list":[1,0,2],"object":{"currency":"USD","value":42.99}}},{"key":"foo"}],"rules":[{"segment":"segment1","distributions":[{"variant":"variant1","rollout":100}]},{"segment":{"keys":["segment1","segment2"],"operator":"AND_SEGMENT_OPERATOR"}}]},{"key":"flag2","name":"flag2","type":"BOOLEAN_FLAG_TYPE","description":"a boolean flag","enabled":false,"rollouts":[{"description":"enabled for internal users","segment":{"key":"internal_users","value":true}},{"description":"enabled for 50%","threshold":{"percentage":50,"value":true}}]}],"segments":[{"key":"segment1","name":"segment1","match_type":"ANY_MATCH_TYPE","description":"description","constraints":[{"type":"STRING_COMPARISON_TYPE","property":"foo","operator":"eq","value":"baz","description":"desc"},{"type":"STRING_COMPARISON_TYPE","property":"fizz","operator":"neq","value":"buzz","description":"desc"}]},{"key":"segment2","name":"segment2","match_type":"ANY_MATCH_TYPE","description":"description"}]} diff --git a/internal/ext/testdata/export_default_and_foo.yml b/internal/ext/testdata/export_default_and_foo.yml index a097bbaabc..9c2920e1fc 100644 --- a/internal/ext/testdata/export_default_and_foo.yml +++ b/internal/ext/testdata/export_default_and_foo.yml @@ -1,5 +1,8 @@ -version: "1.3" -namespace: default +version: "1.4" +namespace: + key: default + name: default + description: default namespace flags: - key: flag1 name: flag1 @@ -69,7 +72,10 @@ segments: match_type: "ANY_MATCH_TYPE" description: description --- -namespace: foo +namespace: + key: foo + name: foo + description: foo namespace flags: - key: flag1 name: flag1 diff --git a/internal/oci/file.go b/internal/oci/file.go index 4145de4143..b7948c33e1 100644 --- a/internal/oci/file.go +++ b/internal/oci/file.go @@ -18,6 +18,7 @@ import ( v1 "github.com/opencontainers/image-spec/specs-go/v1" "go.flipt.io/flipt/internal/containers" "go.flipt.io/flipt/internal/ext" + "go.flipt.io/flipt/internal/storage" storagefs "go.flipt.io/flipt/internal/storage/fs" "go.uber.org/zap" "oras.land/oras-go/v2" @@ -378,16 +379,23 @@ func (s *Store) buildLayers(ctx context.Context, store oras.Target, src fs.FS) ( return err } + var namespaceKey string + if doc.Namespace == nil { + namespaceKey = storage.DefaultNamespace + } else { + namespaceKey = doc.Namespace.GetKey() + } + desc := v1.Descriptor{ Digest: digest.FromBytes(payload), Size: int64(len(payload)), MediaType: MediaTypeFliptNamespace, Annotations: map[string]string{ - AnnotationFliptNamespace: doc.Namespace, + AnnotationFliptNamespace: namespaceKey, }, } - s.logger.Debug("adding layer", zap.String("digest", desc.Digest.Hex()), zap.String("namespace", doc.Namespace)) + s.logger.Debug("adding layer", zap.String("digest", desc.Digest.Hex()), zap.String("namespace", namespaceKey)) if err := store.Push(ctx, desc, bytes.NewReader(payload)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { return err diff --git a/internal/storage/fs/cache_test.go b/internal/storage/fs/cache_test.go index 8f8a3ef965..ddb1659293 100644 --- a/internal/storage/fs/cache_test.go +++ b/internal/storage/fs/cache_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.flipt.io/flipt/internal/ext" "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" "google.golang.org/protobuf/types/known/timestamppb" @@ -27,17 +28,16 @@ const ( ) var ( - snapshotOne = &Snapshot{ns: map[string]*namespace{ - "one": newNamespace("one", "One", timestamppb.Now()), - }} - snapshotTwo = &Snapshot{ns: map[string]*namespace{ - "two": newNamespace("two", "Two", timestamppb.Now()), - }} - snapshotThree = &Snapshot{ns: map[string]*namespace{ - "three": newNamespace("three", "Three", timestamppb.Now()), - }} + snapshotOne = newMockSnapshot("one", "One", timestamppb.Now()) + snapshotTwo = newMockSnapshot("two", "Two", timestamppb.Now()) + snapshotThree = newMockSnapshot("three", "Three", timestamppb.Now()) ) +func newMockSnapshot(key, name string, created *timestamppb.Timestamp) *Snapshot { + n := newNamespace(&ext.NamespaceEmbed{IsNamespace: &ext.Namespace{Key: key, Name: name}}, created) + return &Snapshot{ns: map[string]*namespace{key: n}} +} + func Test_SnapshotCache(t *testing.T) { cache, err := NewSnapshotCache[string](zaptest.NewLogger(t), 2) require.NoError(t, err) diff --git a/internal/storage/fs/snapshot.go b/internal/storage/fs/snapshot.go index 568d1ce36c..927cd3d8dd 100644 --- a/internal/storage/fs/snapshot.go +++ b/internal/storage/fs/snapshot.go @@ -49,13 +49,26 @@ type namespace struct { etag string } -func newNamespace(key, name string, created *timestamppb.Timestamp) *namespace { +func newNamespace(ns *ext.NamespaceEmbed, created *timestamppb.Timestamp) *namespace { + var ( + namespaceName string + namespaceDescription string + ) + + switch t := ns.IsNamespace.(type) { + case ext.NamespaceKey: + namespaceName = string(t) + case *ext.Namespace: + namespaceName = t.Name + namespaceDescription = t.Description + } return &namespace{ resource: &flipt.Namespace{ - Key: key, - Name: name, - CreatedAt: created, - UpdatedAt: created, + Key: ns.GetKey(), + Name: namespaceName, + Description: namespaceDescription, + CreatedAt: created, + UpdatedAt: created, }, flags: map[string]*flipt.Flag{}, segments: map[string]*flipt.Segment{}, @@ -212,7 +225,7 @@ func newSnapshot() *Snapshot { now := flipt.Now() return &Snapshot{ ns: map[string]*namespace{ - defaultNs: newNamespace("default", "Default", now), + defaultNs: newNamespace(ext.DefaultNamespace, now), }, evalDists: map[string][]*storage.EvaluationDistribution{}, now: now, @@ -266,8 +279,8 @@ func documentsFromFile(fi fs.File, opts SnapshotOption) ([]*ext.Document, error) } // set namespace to default if empty in document - if doc.Namespace == "" { - doc.Namespace = "default" + if doc.Namespace == nil { + doc.Namespace = ext.DefaultNamespace } doc.Etag = opts.etagFn(stat) @@ -307,9 +320,13 @@ func listStateFiles(logger *zap.Logger, src fs.FS) ([]string, error) { } func (ss *Snapshot) addDoc(doc *ext.Document) error { - ns := ss.ns[doc.Namespace] + var ( + namespaceKey = doc.Namespace.GetKey() + ns = ss.ns[namespaceKey] + ) + if ns == nil { - ns = newNamespace(doc.Namespace, doc.Namespace, ss.now) + ns = newNamespace(doc.Namespace, ss.now) } evalDists := map[string][]*storage.EvaluationDistribution{} @@ -320,7 +337,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { for _, s := range doc.Segments { matchType := flipt.MatchType_value[s.MatchType] segment := &flipt.Segment{ - NamespaceKey: doc.Namespace, + NamespaceKey: namespaceKey, Name: s.Name, Key: s.Key, Description: s.Description, @@ -332,7 +349,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { for _, constraint := range s.Constraints { constraintType := flipt.ComparisonType_value[constraint.Type] segment.Constraints = append(segment.Constraints, &flipt.Constraint{ - NamespaceKey: doc.Namespace, + NamespaceKey: namespaceKey, SegmentKey: segment.Key, Id: uuid.Must(uuid.NewV4()).String(), Operator: constraint.Operator, @@ -351,7 +368,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { for _, f := range doc.Flags { flagType := flipt.FlagType_value[f.Type] flag := &flipt.Flag{ - NamespaceKey: doc.Namespace, + NamespaceKey: namespaceKey, Key: f.Key, Name: f.Name, Description: f.Description, @@ -369,7 +386,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { variant := &flipt.Variant{ Id: uuid.Must(uuid.NewV4()).String(), - NamespaceKey: doc.Namespace, + NamespaceKey: namespaceKey, Key: v.Key, Name: v.Name, Description: v.Description, @@ -391,7 +408,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { for i, r := range f.Rules { rank := int32(i + 1) rule := &flipt.Rule{ - NamespaceKey: doc.Namespace, + NamespaceKey: namespaceKey, Id: uuid.Must(uuid.NewV4()).String(), FlagKey: f.Key, Rank: rank, @@ -400,7 +417,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { } evalRule := &storage.EvaluationRule{ - NamespaceKey: doc.Namespace, + NamespaceKey: namespaceKey, FlagKey: f.Key, ID: rule.Id, Rank: rank, @@ -492,7 +509,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { for i, rollout := range f.Rollouts { rank := int32(i + 1) s := &storage.EvaluationRollout{ - NamespaceKey: doc.Namespace, + NamespaceKey: namespaceKey, Rank: rank, } @@ -500,7 +517,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { Id: uuid.Must(uuid.NewV4()).String(), Rank: rank, FlagKey: f.Key, - NamespaceKey: doc.Namespace, + NamespaceKey: namespaceKey, CreatedAt: ss.now, UpdatedAt: ss.now, } @@ -589,7 +606,7 @@ func (ss *Snapshot) addDoc(doc *ext.Document) error { ns.evalRollouts[f.Key] = evalRollouts } ns.etag = doc.Etag - ss.ns[doc.Namespace] = ns + ss.ns[namespaceKey] = ns ss.evalDists = evalDists diff --git a/internal/storage/fs/snapshot_test.go b/internal/storage/fs/snapshot_test.go index e074e3447a..6e4bbc7610 100644 --- a/internal/storage/fs/snapshot_test.go +++ b/internal/storage/fs/snapshot_test.go @@ -47,8 +47,8 @@ func TestSnapshotFromFS_Invalid(t *testing.T) { path: "testdata/invalid/namespace", err: errors.Join( validation.Error{Message: "namespace: 2 errors in empty disjunction:", Location: validation.Location{File: "features.json", Line: 1}}, - validation.Error{Message: "namespace: conflicting values 1 and \"default\" (mismatched types int and string)", Location: validation.Location{File: "features.json", Line: 1}}, validation.Error{Message: "namespace: conflicting values 1 and string (mismatched types int and string)", Location: validation.Location{File: "features.json", Line: 1}}, + validation.Error{Message: "namespace: conflicting values 1 and {key:((string & =~\"^[-_,A-Za-z0-9]+$\")|*\"default\"),name?:(string & =~\"^.+$\"),description?:string} (mismatched types int and struct)", Location: validation.Location{File: "features.json", Line: 1}}, ), }, } {