Skip to content

Commit

Permalink
fix(oci): walk entire stream of documents
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgeMac committed Nov 3, 2023
1 parent 5954db6 commit 3e91100
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 87 deletions.
4 changes: 1 addition & 3 deletions cmd/flipt/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import (
"go.flipt.io/flipt/internal/oci"
)

type bundleCommand struct {
rootDir string
}
type bundleCommand struct{}

func newBundleCommand() *cobra.Command {
bundle := &bundleCommand{}
Expand Down
121 changes: 79 additions & 42 deletions internal/ext/common.go
Original file line number Diff line number Diff line change
@@ -1,77 +1,78 @@
package ext

import (
"encoding/json"
"errors"
)

type Document struct {
Version string `yaml:"version,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
Flags []*Flag `yaml:"flags,omitempty"`
Segments []*Segment `yaml:"segments,omitempty"`
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"`
}

type Flag struct {
Key string `yaml:"key,omitempty"`
Name string `yaml:"name,omitempty"`
Type string `yaml:"type,omitempty"`
Description string `yaml:"description,omitempty"`
Enabled bool `yaml:"enabled"`
Variants []*Variant `yaml:"variants,omitempty"`
Rules []*Rule `yaml:"rules,omitempty"`
Rollouts []*Rollout `yaml:"rollouts,omitempty"`
Key string `yaml:"key,omitempty" json:"key,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Type string `yaml:"type,omitempty" json:"type,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Enabled bool `yaml:"enabled" json:"enabled,omitempty"`
Variants []*Variant `yaml:"variants,omitempty" json:"variants,omitempty"`
Rules []*Rule `yaml:"rules,omitempty" json:"rules,omitempty"`
Rollouts []*Rollout `yaml:"rollouts,omitempty" json:"rollouts,omitempty"`
}

type Variant struct {
Key string `yaml:"key,omitempty"`
Name string `yaml:"name,omitempty"`
Description string `yaml:"description,omitempty"`
Attachment interface{} `yaml:"attachment,omitempty"`
Key string `yaml:"key,omitempty" json:"key,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Attachment interface{} `yaml:"attachment,omitempty" json:"attachment,omitempty"`
}

type Rule struct {
Segment *SegmentEmbed `yaml:"segment,omitempty"`
Rank uint `yaml:"rank,omitempty"`
Distributions []*Distribution `yaml:"distributions,omitempty"`
Segment *SegmentEmbed `yaml:"segment,omitempty" json:"segment,omitempty"`
Rank uint `yaml:"rank,omitempty" json:"rank,omitempty"`
Distributions []*Distribution `yaml:"distributions,omitempty" json:"distributions,omitempty"`
}

type Distribution struct {
VariantKey string `yaml:"variant,omitempty"`
Rollout float32 `yaml:"rollout,omitempty"`
VariantKey string `yaml:"variant,omitempty" json:"variant,omitempty"`
Rollout float32 `yaml:"rollout,omitempty" json:"rollout,omitempty"`
}

type Rollout struct {
Description string `yaml:"description,omitempty"`
Segment *SegmentRule `yaml:"segment,omitempty"`
Threshold *ThresholdRule `yaml:"threshold,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Segment *SegmentRule `yaml:"segment,omitempty" json:"segment,omitempty"`
Threshold *ThresholdRule `yaml:"threshold,omitempty" json:"threshold,omitempty"`
}

type SegmentRule struct {
Key string `yaml:"key,omitempty"`
Keys []string `yaml:"keys,omitempty"`
Operator string `yaml:"operator,omitempty"`
Value bool `yaml:"value,omitempty"`
Key string `yaml:"key,omitempty" json:"key,omitempty"`
Keys []string `yaml:"keys,omitempty" json:"keys,omitempty"`
Operator string `yaml:"operator,omitempty" json:"operator,omitempty"`
Value bool `yaml:"value,omitempty" json:"value,omitempty"`
}

type ThresholdRule struct {
Percentage float32 `yaml:"percentage,omitempty"`
Value bool `yaml:"value,omitempty"`
Percentage float32 `yaml:"percentage,omitempty" json:"percentage,omitempty"`
Value bool `yaml:"value,omitempty" json:"value,omitempty"`
}

type Segment struct {
Key string `yaml:"key,omitempty"`
Name string `yaml:"name,omitempty"`
Description string `yaml:"description,omitempty"`
Constraints []*Constraint `yaml:"constraints,omitempty"`
MatchType string `yaml:"match_type,omitempty"`
Key string `yaml:"key,omitempty" json:"key,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Constraints []*Constraint `yaml:"constraints,omitempty" json:"constraints,omitempty"`
MatchType string `yaml:"match_type,omitempty" json:"match_type,omitempty"`
}

type Constraint struct {
Type string `yaml:"type,omitempty"`
Property string `yaml:"property,omitempty"`
Operator string `yaml:"operator,omitempty"`
Value string `yaml:"value,omitempty"`
Description string `yaml:"description,omitempty"`
Type string `yaml:"type,omitempty" json:"type,omitempty"`
Property string `yaml:"property,omitempty" json:"property,omitempty"`
Operator string `yaml:"operator,omitempty" json:"operator,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
}

type SegmentEmbed struct {
Expand Down Expand Up @@ -114,6 +115,42 @@ func (s *SegmentEmbed) UnmarshalYAML(unmarshal func(interface{}) error) error {
return errors.New("failed to unmarshal to string or segmentKeys")
}

// MarshalJSON tries to type assert to either of the following types that implement
// IsSegment, and returns the marshaled value.
func (s *SegmentEmbed) MarshalJSON() ([]byte, error) {
switch t := s.IsSegment.(type) {
case SegmentKey:
return json.Marshal(string(t))
case *Segments:
sk := &Segments{
Keys: t.Keys,
SegmentOperator: t.SegmentOperator,
}
return json.Marshal(sk)

Check warning on line 129 in internal/ext/common.go

View check run for this annotation

Codecov / codecov/patch

internal/ext/common.go#L120-L129

Added lines #L120 - L129 were not covered by tests
}

return nil, errors.New("failed to marshal to string or segmentKeys")

Check warning on line 132 in internal/ext/common.go

View check run for this annotation

Codecov / codecov/patch

internal/ext/common.go#L132

Added line #L132 was not covered by tests
}

// UnmarshalJSON attempts to unmarshal a string or `SegmentKeys`, and fails if it can not
// do so.
func (s *SegmentEmbed) UnmarshalJSON(v []byte) error {
var sk SegmentKey

if err := json.Unmarshal(v, &sk); err == nil {
s.IsSegment = sk
return nil
}

Check warning on line 143 in internal/ext/common.go

View check run for this annotation

Codecov / codecov/patch

internal/ext/common.go#L137-L143

Added lines #L137 - L143 were not covered by tests

var sks *Segments
if err := json.Unmarshal(v, &sks); err == nil {
s.IsSegment = sks
return nil
}

Check warning on line 149 in internal/ext/common.go

View check run for this annotation

Codecov / codecov/patch

internal/ext/common.go#L145-L149

Added lines #L145 - L149 were not covered by tests

return errors.New("failed to unmarshal to string or segmentKeys")

Check warning on line 151 in internal/ext/common.go

View check run for this annotation

Codecov / codecov/patch

internal/ext/common.go#L151

Added line #L151 was not covered by tests
}

// IsSegment is used to unify the two types of segments that can come in
// from the import.
type IsSegment interface {
Expand All @@ -125,8 +162,8 @@ type SegmentKey string
func (s SegmentKey) IsSegment() {}

type Segments struct {
Keys []string `yaml:"keys,omitempty"`
SegmentOperator string `yaml:"operator,omitempty"`
Keys []string `yaml:"keys,omitempty" json:"keys,omitempty"`
SegmentOperator string `yaml:"operator,omitempty" json:"operator,omitempty"`
}

func (s *Segments) IsSegment() {}
10 changes: 5 additions & 5 deletions internal/oci/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,25 +285,25 @@ type Bundle struct {
func (s *Store) Build(ctx context.Context, src fs.FS) (Bundle, error) {
store, err := s.store()
if err != nil {
return Bundle{}, nil
return Bundle{}, err
}

Check warning on line 289 in internal/oci/file.go

View check run for this annotation

Codecov / codecov/patch

internal/oci/file.go#L288-L289

Added lines #L288 - L289 were not covered by tests

layers, err := s.buildLayers(ctx, store, src)
if err != nil {
return Bundle{}, nil
return Bundle{}, err
}

Check warning on line 294 in internal/oci/file.go

View check run for this annotation

Codecov / codecov/patch

internal/oci/file.go#L293-L294

Added lines #L293 - L294 were not covered by tests

desc, err := oras.PackManifest(ctx, store, oras.PackManifestVersion1_1_RC4, MediaTypeFliptFeatures, oras.PackManifestOptions{
ManifestAnnotations: map[string]string{},
Layers: layers,
})
if err != nil {
return Bundle{}, nil
return Bundle{}, err
}

Check warning on line 302 in internal/oci/file.go

View check run for this annotation

Codecov / codecov/patch

internal/oci/file.go#L301-L302

Added lines #L301 - L302 were not covered by tests

if s.reference.Reference != "" {
if err := store.Tag(ctx, desc, s.reference.Reference); err != nil {
return Bundle{}, nil
return Bundle{}, err
}

Check warning on line 307 in internal/oci/file.go

View check run for this annotation

Codecov / codecov/patch

internal/oci/file.go#L306-L307

Added lines #L306 - L307 were not covered by tests
}

Expand All @@ -315,7 +315,7 @@ func (s *Store) Build(ctx context.Context, src fs.FS) (Bundle, error) {

bundle.CreatedAt, err = parseCreated(desc.Annotations)
if err != nil {
return Bundle{}, nil
return Bundle{}, err
}

Check warning on line 319 in internal/oci/file.go

View check run for this annotation

Codecov / codecov/patch

internal/oci/file.go#L318-L319

Added lines #L318 - L319 were not covered by tests

return bundle, nil
Expand Down
17 changes: 9 additions & 8 deletions internal/oci/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"oras.land/oras-go/v2/content/oci"
)

const repo = "testrepo"

func TestNewStore(t *testing.T) {
t.Run("unexpected scheme", func(t *testing.T) {
_, err := NewStore(zaptest.NewLogger(t), "fake://local/something:latest")
Expand Down Expand Up @@ -51,7 +53,7 @@ func TestNewStore(t *testing.T) {
}

func TestStore_Fetch_InvalidMediaType(t *testing.T) {
dir, repo := testRepository(t,
dir := testRepository(t,
layer("default", `{"namespace":"default"}`, "unexpected.media.type"),
)

Expand All @@ -63,7 +65,7 @@ func TestStore_Fetch_InvalidMediaType(t *testing.T) {
_, err = store.Fetch(ctx)
require.EqualError(t, err, "layer \"sha256:85ee577ad99c62f314abca9f43ad87c2ee8818513e6383a77690df56d0352748\": type \"unexpected.media.type\": unexpected media type")

dir, repo = testRepository(t,
dir = testRepository(t,
layer("default", `{"namespace":"default"}`, MediaTypeFliptNamespace+"+unknown"),
)

Expand All @@ -75,7 +77,7 @@ func TestStore_Fetch_InvalidMediaType(t *testing.T) {
}

func TestStore_Fetch(t *testing.T) {
dir, repo := testRepository(t,
dir := testRepository(t,
layer("default", `{"namespace":"default"}`, MediaTypeFliptNamespace),
layer("other", `namespace: other`, MediaTypeFliptNamespace+"+yaml"),
)
Expand Down Expand Up @@ -129,7 +131,7 @@ var testdata embed.FS

func TestStore_Build(t *testing.T) {
ctx := context.TODO()
dir, repo := testRepository(t)
dir := testRepository(t)

store, err := NewStore(zaptest.NewLogger(t), fmt.Sprintf("flipt://local/%s:latest", repo), WithBundleDir(dir))
require.NoError(t, err)
Expand Down Expand Up @@ -171,15 +173,14 @@ func layer(ns, payload, mediaType string) func(*testing.T, oras.Target) v1.Descr
}
}

func testRepository(t *testing.T, layerFuncs ...func(*testing.T, oras.Target) v1.Descriptor) (dir, repository string) {
func testRepository(t *testing.T, layerFuncs ...func(*testing.T, oras.Target) v1.Descriptor) (dir string) {
t.Helper()

repository = "testrepo"
dir = t.TempDir()

t.Log("test OCI directory", dir, repository)
t.Log("test OCI directory", dir, repo)

store, err := oci.New(path.Join(dir, repository))
store, err := oci.New(path.Join(dir, repo))
require.NoError(t, err)

store.AutoSaveIndex = true
Expand Down
57 changes: 28 additions & 29 deletions internal/storage/fs/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,15 @@ func SnapshotFromFiles(logger *zap.Logger, files ...fs.File) (*StoreSnapshot, er
for _, fi := range files {
defer fi.Close()

doc, err := documentFromFile(logger, fi)
docs, err := documentsFromFile(fi)
if err != nil {
return nil, err
}

Check warning on line 128 in internal/storage/fs/snapshot.go

View check run for this annotation

Codecov / codecov/patch

internal/storage/fs/snapshot.go#L127-L128

Added lines #L127 - L128 were not covered by tests

if err := s.addDoc(doc); err != nil {
return nil, err
for _, doc := range docs {
if err := s.addDoc(doc); err != nil {
return nil, err
}
}
}

Expand All @@ -152,21 +154,23 @@ func WalkDocuments(logger *zap.Logger, src fs.FS, fn func(*ext.Document) error)
}
defer fi.Close()

doc, err := documentFromFile(logger, fi)
docs, err := documentsFromFile(fi)
if err != nil {
return err
}

Check warning on line 160 in internal/storage/fs/snapshot.go

View check run for this annotation

Codecov / codecov/patch

internal/storage/fs/snapshot.go#L151-L160

Added lines #L151 - L160 were not covered by tests

if err := fn(doc); err != nil {
return err
for _, doc := range docs {
if err := fn(doc); err != nil {

Check warning on line 163 in internal/storage/fs/snapshot.go

View check run for this annotation

Codecov / codecov/patch

internal/storage/fs/snapshot.go#L162-L163

Added lines #L162 - L163 were not covered by tests
return err
}
}
}

return nil

Check warning on line 169 in internal/storage/fs/snapshot.go

View check run for this annotation

Codecov / codecov/patch

internal/storage/fs/snapshot.go#L169

Added line #L169 was not covered by tests
}

// documentFromFile parses and validates a document from a single fs.File instance
func documentFromFile(logger *zap.Logger, fi fs.File) (*ext.Document, error) {
// documentsFromFile parses and validates a document from a single fs.File instance
func documentsFromFile(fi fs.File) ([]*ext.Document, error) {
validator, err := cue.NewFeaturesValidator()
if err != nil {
return nil, err
Expand All @@ -184,42 +188,37 @@ func documentFromFile(logger *zap.Logger, fi fs.File) (*ext.Document, error) {
return nil, err
}

Check warning on line 189 in internal/storage/fs/snapshot.go

View check run for this annotation

Codecov / codecov/patch

internal/storage/fs/snapshot.go#L188-L189

Added lines #L188 - L189 were not covered by tests

var docs []*ext.Document
extn := filepath.Ext(stat.Name())

var decode func(any) error
switch extn {
case ".yaml", ".yml":
decoder := yaml.NewDecoder(buf)
// Support YAML stream by looping until we reach an EOF.
for {
doc := &ext.Document{}
if err := decoder.Decode(doc); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, err
}

// set namespace to default if empty in document
if doc.Namespace == "" {
doc.Namespace = "default"
}

return doc, nil
}
decode = yaml.NewDecoder(buf).Decode
case "", ".json":
decode = json.NewDecoder(buf).Decode
default:
return nil, fmt.Errorf("unexpected extension: %q", extn)

Check warning on line 202 in internal/storage/fs/snapshot.go

View check run for this annotation

Codecov / codecov/patch

internal/storage/fs/snapshot.go#L199-L202

Added lines #L199 - L202 were not covered by tests
}

for {
doc := &ext.Document{}
if err := json.NewDecoder(buf).Decode(&doc); err != nil {
if err := decode(doc); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, err
}

// set namespace to default if empty in document
if doc.Namespace == "" {
doc.Namespace = "default"
}

Check warning on line 217 in internal/storage/fs/snapshot.go

View check run for this annotation

Codecov / codecov/patch

internal/storage/fs/snapshot.go#L216-L217

Added lines #L216 - L217 were not covered by tests

return doc, nil
docs = append(docs, doc)
}

return nil, fmt.Errorf("unexpected extension: %q", extn)
return docs, nil
}

// listStateFiles lists all the file paths in a provided fs.FS containing Flipt feature state
Expand Down

0 comments on commit 3e91100

Please sign in to comment.