Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Etag version #3248

Merged
merged 22 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
"import/export",
"authn",
"authz",
"rest",
]
steps:
- uses: actions/checkout@v4
Expand Down
7 changes: 7 additions & 0 deletions build/testing/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ var (
"import/export": importExport,
"authn": authn,
"authz": authz,
"rest": rest,
}
)

Expand Down Expand Up @@ -269,6 +270,12 @@ func api(ctx context.Context, _ *dagger.Client, base, flipt *dagger.Container, c
flipt.WithEnvVariable("UNIQUE", uuid.New().String()).WithExec(nil), conf)
}

func rest(ctx context.Context, _ *dagger.Client, base, flipt *dagger.Container, conf testConfig) func() error {
return suite(ctx, "rest", base,
// create unique instance for test case
flipt.WithEnvVariable("UNIQUE", uuid.New().String()).WithExec(nil), conf)
}

func withSQLite(fn testCaseFn) testCaseFn {
return fn
}
Expand Down
36 changes: 36 additions & 0 deletions build/testing/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/pem"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"strings"
Expand Down Expand Up @@ -134,6 +135,41 @@ func WithRole(role string) ClientOpt {
}
}

type roundTripFunc func(r *http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}

func (o TestOpts) HTTPClient(t *testing.T, opts ...ClientOpt) *http.Client {
t.Helper()

var copts ClientOpts
for _, opt := range opts {
opt(&copts)
}

metadata := map[string]string{}
if copts.Role != "" {
metadata["io.flipt.auth.role"] = copts.Role
}

resp, err := o.bootstrapClient(t).Auth().AuthenticationMethodTokenService().CreateToken(context.Background(), &auth.CreateTokenRequest{
Name: t.Name(),
NamespaceKey: copts.Namespace,
Metadata: metadata,
})

require.NoError(t, err)

transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", resp.ClientToken))
return http.DefaultTransport.RoundTrip(r)
})

return &http.Client{Transport: transport}
}

func (o TestOpts) TokenClient(t *testing.T, opts ...ClientOpt) sdk.SDK {
t.Helper()

Expand Down
119 changes: 119 additions & 0 deletions build/testing/integration/rest/rest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package rest

import (
"context"
"fmt"
"io"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.flipt.io/build/testing/integration"
"go.flipt.io/flipt/rpc/flipt"
)

// REST tests the REST API without using the SDK.
func REST(t *testing.T, ctx context.Context, opts integration.TestOpts) {
var (
client = opts.TokenClient(t)
httpClient = opts.HTTPClient(t)
protocol = opts.Protocol()
)

if protocol == integration.ProtocolGRPC {
t.Skip("REST tests are not applicable for gRPC")
}

t.Run("Evaluation Data", func(t *testing.T) {
t.Log(`Create namespace.`)

_, err := client.Flipt().CreateNamespace(ctx, &flipt.CreateNamespaceRequest{
Key: integration.ProductionNamespace,
Name: "Production",
})
require.NoError(t, err)

for _, namespace := range integration.Namespaces {
t.Run(fmt.Sprintf("namespace %q", namespace.Expected), func(t *testing.T) {
// create some flags
_, err = client.Flipt().CreateFlag(ctx, &flipt.CreateFlagRequest{
NamespaceKey: namespace.Key,
Key: "test",
Name: "Test",
Description: "This is a test flag",
Enabled: true,
})
require.NoError(t, err)

t.Log("Create a new flag in a disabled state.")

_, err = client.Flipt().CreateFlag(ctx, &flipt.CreateFlagRequest{
NamespaceKey: namespace.Key,
Key: "disabled",
Name: "Disabled",
Description: "This is a disabled test flag",
Enabled: false,
})
require.NoError(t, err)

t.Log("Create a new enabled boolean flag with key \"boolean_enabled\".")

_, err = client.Flipt().CreateFlag(ctx, &flipt.CreateFlagRequest{
Type: flipt.FlagType_BOOLEAN_FLAG_TYPE,
NamespaceKey: namespace.Key,
Key: "boolean_enabled",
Name: "Boolean Enabled",
Description: "This is an enabled boolean test flag",
Enabled: true,
})
require.NoError(t, err)

t.Log("Create a new flag in a disabled state.")

_, err = client.Flipt().CreateFlag(ctx, &flipt.CreateFlagRequest{
Type: flipt.FlagType_BOOLEAN_FLAG_TYPE,
NamespaceKey: namespace.Key,
Key: "boolean_disabled",
Name: "Boolean Disabled",
Description: "This is a disabled boolean test flag",
Enabled: false,
})
require.NoError(t, err)

t.Logf("Get snapshot for namespace.")

resp, err := httpClient.Get(fmt.Sprintf("%s/internal/v1/evaluation/snapshot/namespace/%s", opts.URL, namespace))

require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
defer safeClose(resp.Body)

// get etag from response
etag := resp.Header.Get("ETag")
assert.NotEmpty(t, etag)

// read body
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

assert.NotEmpty(t, body)

req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/internal/v1/evaluation/snapshot/namespace/%s", opts.URL, namespace), nil)
req.Header.Set("If-None-Match", etag)
require.NoError(t, err)

resp, err = httpClient.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusNotModified, resp.StatusCode)
safeClose(resp.Body)
})
}
})
}

func safeClose(r io.ReadCloser) {
if r != nil {
_ = r.Close()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE namespaces ADD COLUMN state_modified_at TIMESTAMP;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE namespaces ADD COLUMN state_modified_at TIMESTAMP;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE namespaces ADD COLUMN state_modified_at TIMESTAMP;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE namespaces ADD COLUMN state_modified_at TIMESTAMP;
5 changes: 1 addition & 4 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,6 @@ github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR
github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk=
github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
Expand Down Expand Up @@ -278,6 +276,7 @@ github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
Expand Down Expand Up @@ -417,8 +416,6 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615/go.mod h1:Ad7oeElCZqA1Ufj0U9/liOF4BtVepxRcTvr2ey7zTvM=
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
Expand Down
4 changes: 3 additions & 1 deletion internal/cmd/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"go.flipt.io/flipt/internal/info"
"go.flipt.io/flipt/internal/server/authn/method"
grpc_middleware "go.flipt.io/flipt/internal/server/middleware/grpc"
http_middleware "go.flipt.io/flipt/internal/server/middleware/http"
"go.flipt.io/flipt/rpc/flipt"
"go.flipt.io/flipt/rpc/flipt/analytics"
"go.flipt.io/flipt/rpc/flipt/evaluation"
Expand Down Expand Up @@ -64,7 +65,7 @@
r = chi.NewRouter()
api = gateway.NewGatewayServeMux(logger)
evaluateAPI = gateway.NewGatewayServeMux(logger)
evaluateDataAPI = gateway.NewGatewayServeMux(logger, runtime.WithMetadata(grpc_middleware.ForwardFliptAcceptServerVersion))
evaluateDataAPI = gateway.NewGatewayServeMux(logger, runtime.WithMetadata(grpc_middleware.ForwardFliptAcceptServerVersion), runtime.WithForwardResponseOption(http_middleware.HttpResponseModifier))

Check warning on line 68 in internal/cmd/http.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/http.go#L68

Added line #L68 was not covered by tests
analyticsAPI = gateway.NewGatewayServeMux(logger)
ofrepAPI = gateway.NewGatewayServeMux(logger)
httpPort = cfg.Server.HTTPPort
Expand Down Expand Up @@ -127,6 +128,7 @@
})
})
r.Use(middleware.Compress(gzip.DefaultCompression))
r.Use(http_middleware.HandleNoBodyResponse)

Check warning on line 131 in internal/cmd/http.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/http.go#L131

Added line #L131 was not covered by tests
r.Use(middleware.Recoverer)

if cfg.Diagnostics.Profiling.Enabled {
Expand Down
5 changes: 5 additions & 0 deletions internal/common/store_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
return "mock"
}

func (m *StoreMock) GetVersion(ctx context.Context, ns storage.NamespaceRequest) (string, error) {
args := m.Called(ctx)
return args.String(0), args.Error(1)

Check warning on line 23 in internal/common/store_mock.go

View check run for this annotation

Codecov / codecov/patch

internal/common/store_mock.go#L21-L23

Added lines #L21 - L23 were not covered by tests
}

func (m *StoreMock) GetNamespace(ctx context.Context, ns storage.NamespaceRequest) (*flipt.Namespace, error) {
args := m.Called(ctx, ns)
return args.Get(0).(*flipt.Namespace), args.Error(1)
Expand Down
2 changes: 1 addition & 1 deletion internal/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ func NewGatewayServeMux(logger *zap.Logger, opts ...runtime.ServeMuxOption) *run

})

return runtime.NewServeMux(append(commonMuxOptions, opts...)...)
return runtime.NewServeMux(append(opts, commonMuxOptions...)...)
}
49 changes: 49 additions & 0 deletions internal/server/evaluation/data/evaluation_store_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package data

import (
"context"

"github.com/stretchr/testify/mock"
"go.flipt.io/flipt/internal/storage"
flipt "go.flipt.io/flipt/rpc/flipt"
)

var _ EvaluationStore = &evaluationStoreMock{}

type evaluationStoreMock struct {
mock.Mock
}

func (e *evaluationStoreMock) String() string {
return "mock"

Check warning on line 18 in internal/server/evaluation/data/evaluation_store_mock.go

View check run for this annotation

Codecov / codecov/patch

internal/server/evaluation/data/evaluation_store_mock.go#L17-L18

Added lines #L17 - L18 were not covered by tests
}

func (e *evaluationStoreMock) GetVersion(ctx context.Context, ns storage.NamespaceRequest) (string, error) {
args := e.Called(ctx, ns)
return args.String(0), args.Error(1)
}

func (e *evaluationStoreMock) ListFlags(ctx context.Context, req *storage.ListRequest[storage.NamespaceRequest]) (storage.ResultSet[*flipt.Flag], error) {
args := e.Called(ctx, req)
return args.Get(0).(storage.ResultSet[*flipt.Flag]), args.Error(1)

Check warning on line 28 in internal/server/evaluation/data/evaluation_store_mock.go

View check run for this annotation

Codecov / codecov/patch

internal/server/evaluation/data/evaluation_store_mock.go#L26-L28

Added lines #L26 - L28 were not covered by tests
}

func (e *evaluationStoreMock) GetFlag(ctx context.Context, flag storage.ResourceRequest) (*flipt.Flag, error) {
args := e.Called(ctx, flag)
return args.Get(0).(*flipt.Flag), args.Error(1)

Check warning on line 33 in internal/server/evaluation/data/evaluation_store_mock.go

View check run for this annotation

Codecov / codecov/patch

internal/server/evaluation/data/evaluation_store_mock.go#L31-L33

Added lines #L31 - L33 were not covered by tests
}

func (e *evaluationStoreMock) GetEvaluationRules(ctx context.Context, flag storage.ResourceRequest) ([]*storage.EvaluationRule, error) {
args := e.Called(ctx, flag)
return args.Get(0).([]*storage.EvaluationRule), args.Error(1)

Check warning on line 38 in internal/server/evaluation/data/evaluation_store_mock.go

View check run for this annotation

Codecov / codecov/patch

internal/server/evaluation/data/evaluation_store_mock.go#L36-L38

Added lines #L36 - L38 were not covered by tests
}

func (e *evaluationStoreMock) GetEvaluationDistributions(ctx context.Context, ruleID storage.IDRequest) ([]*storage.EvaluationDistribution, error) {
args := e.Called(ctx, ruleID)
return args.Get(0).([]*storage.EvaluationDistribution), args.Error(1)

Check warning on line 43 in internal/server/evaluation/data/evaluation_store_mock.go

View check run for this annotation

Codecov / codecov/patch

internal/server/evaluation/data/evaluation_store_mock.go#L41-L43

Added lines #L41 - L43 were not covered by tests
}

func (e *evaluationStoreMock) GetEvaluationRollouts(ctx context.Context, flag storage.ResourceRequest) ([]*storage.EvaluationRollout, error) {
args := e.Called(ctx, flag)
return args.Get(0).([]*storage.EvaluationRollout), args.Error(1)

Check warning on line 48 in internal/server/evaluation/data/evaluation_store_mock.go

View check run for this annotation

Codecov / codecov/patch

internal/server/evaluation/data/evaluation_store_mock.go#L46-L48

Added lines #L46 - L48 were not covered by tests
}
42 changes: 41 additions & 1 deletion internal/server/evaluation/data/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import (
"context"
"crypto/sha1" //nolint:gosec

"fmt"

"github.com/blang/semver/v4"
Expand All @@ -11,11 +13,13 @@
"go.flipt.io/flipt/rpc/flipt/evaluation"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

type EvaluationStore interface {
ListFlags(ctx context.Context, req *storage.ListRequest[storage.NamespaceRequest]) (storage.ResultSet[*flipt.Flag], error)
storage.EvaluationStore
storage.NamespaceVersionStore
}

type Server struct {
Expand Down Expand Up @@ -96,10 +100,46 @@
var supportsEntityIdConstraintMinVersion = semver.MustParse("1.38.0")

func (srv *Server) EvaluationSnapshotNamespace(ctx context.Context, r *evaluation.EvaluationNamespaceSnapshotRequest) (*evaluation.EvaluationNamespaceSnapshot, error) {

var (
namespaceKey = r.Key
reference = r.Reference
resp = &evaluation.EvaluationNamespaceSnapshot{
ifNoneMatch string
)

md, ok := metadata.FromIncomingContext(ctx)
if ok {
// get If-None-Match header from request
if vals := md.Get("GrpcGateway-If-None-Match"); len(vals) > 0 {
ifNoneMatch = vals[0]
}
}

// get current version from store to calculate etag for this namespace
currentVersion, err := srv.store.GetVersion(ctx, storage.NewNamespace(namespaceKey))
if err != nil {
srv.logger.Error("getting current version", zap.Error(err))

Check warning on line 121 in internal/server/evaluation/data/server.go

View check run for this annotation

Codecov / codecov/patch

internal/server/evaluation/data/server.go#L121

Added line #L121 was not covered by tests
}

if currentVersion != "" {
var (
hash = sha1.New() //nolint:gosec
_, _ = hash.Write([]byte(currentVersion))
// etag is the sha1 hash of the current version
etag = fmt.Sprintf("%x", hash.Sum(nil))
)

// set etag header in the response
_ = grpc.SetHeader(ctx, metadata.Pairs("x-etag", etag))
// if etag matches the If-None-Match header, we want to return a 304
if ifNoneMatch == etag {
_ = grpc.SetHeader(ctx, metadata.Pairs("x-http-code", "304"))
return nil, nil
}
}

var (
resp = &evaluation.EvaluationNamespaceSnapshot{

Check warning on line 142 in internal/server/evaluation/data/server.go

View check run for this annotation

Codecov / codecov/patch

internal/server/evaluation/data/server.go#L141-L142

Added lines #L141 - L142 were not covered by tests
Namespace: &evaluation.EvaluationNamespace{ // TODO: should we get from store?
Key: namespaceKey,
},
Expand Down
Loading
Loading