diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 558d5c6b1e..254fccbaec 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -56,6 +56,7 @@ jobs: "import/export", "authn", "authz", + "rest", ] steps: - uses: actions/checkout@v4 diff --git a/build/testing/integration.go b/build/testing/integration.go index 1bfd216e8c..5c82911eb3 100644 --- a/build/testing/integration.go +++ b/build/testing/integration.go @@ -66,6 +66,7 @@ var ( "import/export": importExport, "authn": authn, "authz": authz, + "rest": rest, } ) @@ -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 } diff --git a/build/testing/integration/integration.go b/build/testing/integration/integration.go index ac3a5b720e..eacde6a108 100644 --- a/build/testing/integration/integration.go +++ b/build/testing/integration/integration.go @@ -6,6 +6,7 @@ import ( "encoding/pem" "flag" "fmt" + "net/http" "net/url" "os" "strings" @@ -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() diff --git a/build/testing/integration/rest/rest.go b/build/testing/integration/rest/rest.go new file mode 100644 index 0000000000..1a3c4ccae2 --- /dev/null +++ b/build/testing/integration/rest/rest.go @@ -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() + } +} diff --git a/config/migrations/cockroachdb/10_namespaces_add_state_modified_at.up.sql b/config/migrations/cockroachdb/10_namespaces_add_state_modified_at.up.sql new file mode 100644 index 0000000000..1d8bb69620 --- /dev/null +++ b/config/migrations/cockroachdb/10_namespaces_add_state_modified_at.up.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces ADD COLUMN state_modified_at TIMESTAMP; diff --git a/config/migrations/mysql/12_namespaces_add_state_modified_at.up.sql b/config/migrations/mysql/12_namespaces_add_state_modified_at.up.sql new file mode 100644 index 0000000000..1d8bb69620 --- /dev/null +++ b/config/migrations/mysql/12_namespaces_add_state_modified_at.up.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces ADD COLUMN state_modified_at TIMESTAMP; diff --git a/config/migrations/postgres/13_namespaces_add_state_modified_at.up.sql b/config/migrations/postgres/13_namespaces_add_state_modified_at.up.sql new file mode 100644 index 0000000000..1d8bb69620 --- /dev/null +++ b/config/migrations/postgres/13_namespaces_add_state_modified_at.up.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces ADD COLUMN state_modified_at TIMESTAMP; diff --git a/config/migrations/sqlite3/12_namespaces_add_state_modified_at.up.sql b/config/migrations/sqlite3/12_namespaces_add_state_modified_at.up.sql new file mode 100644 index 0000000000..1d8bb69620 --- /dev/null +++ b/config/migrations/sqlite3/12_namespaces_add_state_modified_at.up.sql @@ -0,0 +1 @@ +ALTER TABLE namespaces ADD COLUMN state_modified_at TIMESTAMP; diff --git a/go.work.sum b/go.work.sum index 591d79af5b..d0e4e81fcf 100644 --- a/go.work.sum +++ b/go.work.sum @@ -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= @@ -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= @@ -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= diff --git a/internal/cmd/http.go b/internal/cmd/http.go index e1772b8659..b0b03d59dc 100644 --- a/internal/cmd/http.go +++ b/internal/cmd/http.go @@ -24,6 +24,7 @@ import ( "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" @@ -64,7 +65,7 @@ func NewHTTPServer( 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)) analyticsAPI = gateway.NewGatewayServeMux(logger) ofrepAPI = gateway.NewGatewayServeMux(logger) httpPort = cfg.Server.HTTPPort @@ -127,6 +128,7 @@ func NewHTTPServer( }) }) r.Use(middleware.Compress(gzip.DefaultCompression)) + r.Use(http_middleware.HandleNoBodyResponse) r.Use(middleware.Recoverer) if cfg.Diagnostics.Profiling.Enabled { diff --git a/internal/common/store_mock.go b/internal/common/store_mock.go index 6483f0b9af..dea2599064 100644 --- a/internal/common/store_mock.go +++ b/internal/common/store_mock.go @@ -18,6 +18,11 @@ func (m *StoreMock) String() string { 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) +} + 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) diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index c25b820c5d..0e64deb707 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -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...)...) } diff --git a/internal/server/evaluation/data/evaluation_store_mock.go b/internal/server/evaluation/data/evaluation_store_mock.go new file mode 100644 index 0000000000..56efa3fa81 --- /dev/null +++ b/internal/server/evaluation/data/evaluation_store_mock.go @@ -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" +} + +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) +} + +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) +} + +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) +} + +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) +} + +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) +} diff --git a/internal/server/evaluation/data/server.go b/internal/server/evaluation/data/server.go index 328eef8cf6..cfdbc721c8 100644 --- a/internal/server/evaluation/data/server.go +++ b/internal/server/evaluation/data/server.go @@ -2,6 +2,8 @@ package data import ( "context" + "crypto/sha1" //nolint:gosec + "fmt" "github.com/blang/semver/v4" @@ -11,11 +13,13 @@ import ( "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 { @@ -96,10 +100,46 @@ func toEvaluationRolloutType(r flipt.RolloutType) evaluation.EvaluationRolloutTy 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)) + } + + 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{ Namespace: &evaluation.EvaluationNamespace{ // TODO: should we get from store? Key: namespaceKey, }, diff --git a/internal/server/evaluation/data/server_test.go b/internal/server/evaluation/data/server_test.go new file mode 100644 index 0000000000..86c04b7c54 --- /dev/null +++ b/internal/server/evaluation/data/server_test.go @@ -0,0 +1,36 @@ +package data + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.flipt.io/flipt/rpc/flipt/evaluation" + "go.uber.org/zap/zaptest" + "google.golang.org/grpc/metadata" +) + +func TestEvaluationSnapshotNamespace(t *testing.T) { + var ( + store = &evaluationStoreMock{} + logger = zaptest.NewLogger(t) + s = New(logger, store) + ) + + t.Run("If-None-Match header match", func(t *testing.T) { + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("GrpcGateway-If-None-Match", "92e200311a56800b3e475bf2d2442724535e87bf")) + + store.On("GetVersion", mock.Anything, mock.Anything).Return("etag", nil) + + resp, err := s.EvaluationSnapshotNamespace(ctx, &evaluation.EvaluationNamespaceSnapshotRequest{ + Key: "namespace", + }) + + require.NoError(t, err) + assert.Nil(t, resp) + + store.AssertExpectations(t) + }) +} diff --git a/internal/server/middleware/http/middleware.go b/internal/server/middleware/http/middleware.go new file mode 100644 index 0000000000..39edbb305a --- /dev/null +++ b/internal/server/middleware/http/middleware.go @@ -0,0 +1,67 @@ +package http_middleware + +import ( + "context" + "net/http" + "strconv" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/protobuf/proto" +) + +func HttpResponseModifier(ctx context.Context, w http.ResponseWriter, _ proto.Message) error { + md, ok := runtime.ServerMetadataFromContext(ctx) + if !ok { + return nil + } + + // set etag header if it exists + if vals := md.HeaderMD.Get("x-etag"); len(vals) > 0 { + // delete the headers to not expose any grpc-metadata in http response + delete(md.HeaderMD, "x-etag") + delete(w.Header(), "Grpc-Metadata-X-Etag") + w.Header().Set("Etag", vals[0]) + } + + // check if we set a custom status code + if vals := md.HeaderMD.Get("x-http-code"); len(vals) > 0 { + // delete the headers to not expose any grpc-metadata in http response + delete(md.HeaderMD, "x-http-code") + delete(w.Header(), "Grpc-Metadata-X-Http-Code") + + code, _ := strconv.Atoi(vals[0]) + w.WriteHeader(code) + } + + return nil +} + +// HandleNoBodyResponse is a response modifier that does not write a body if the response is a 204 No Content or 304 Not Modified. +func HandleNoBodyResponse(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nmw := &noBodyResponseWriter{ResponseWriter: w} + next.ServeHTTP(nmw, r) + }) +} + +type noBodyResponseWriter struct { + wroteHeader bool + code int + http.ResponseWriter +} + +func (w *noBodyResponseWriter) WriteHeader(code int) { + w.code = code + w.wroteHeader = true + w.ResponseWriter.WriteHeader(code) +} + +func (w *noBodyResponseWriter) Write(b []byte) (int, error) { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + if w.code == http.StatusNotModified || w.code == http.StatusNoContent { + return 0, nil + } + return w.ResponseWriter.Write(b) +} diff --git a/internal/server/middleware/http/middleware_test.go b/internal/server/middleware/http/middleware_test.go new file mode 100644 index 0000000000..4f8a64e114 --- /dev/null +++ b/internal/server/middleware/http/middleware_test.go @@ -0,0 +1,67 @@ +package http_middleware + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + pb "google.golang.org/protobuf/types/known/emptypb" +) + +func TestHttpResponseModifier(t *testing.T) { + t.Run("etag header exists", func(t *testing.T) { + md := runtime.ServerMetadata{ + HeaderMD: metadata.Pairs( + "foo", "bar", + "baz", "qux", + "x-etag", "etag", + ), + } + + var ( + ctx = runtime.NewServerMetadataContext(context.Background(), md) + resp = httptest.NewRecorder() + msg = &pb.Empty{} + ) + + err := HttpResponseModifier(ctx, resp, msg) + require.NoError(t, err) + + w := resp.Result() + defer w.Body.Close() + + assert.NotEmpty(t, w.Header) + + assert.Equal(t, "etag", w.Header.Get("Etag")) + assert.Empty(t, w.Header.Get("Grpc-Metadata-X-Etag")) + }) + + t.Run("http code header exists", func(t *testing.T) { + md := runtime.ServerMetadata{ + HeaderMD: metadata.Pairs( + "foo", "bar", + "baz", "qux", + "x-http-code", "300", + ), + } + + var ( + ctx = runtime.NewServerMetadataContext(context.Background(), md) + resp = httptest.NewRecorder() + msg = &pb.Empty{} + ) + + err := HttpResponseModifier(ctx, resp, msg) + require.NoError(t, err) + + w := resp.Result() + defer w.Body.Close() + + assert.Empty(t, w.Header) + assert.Equal(t, 300, w.StatusCode) + }) +} diff --git a/internal/storage/fs/object/store.go b/internal/storage/fs/object/store.go index 814c0c40ac..85df5b01ff 100644 --- a/internal/storage/fs/object/store.go +++ b/internal/storage/fs/object/store.go @@ -161,3 +161,8 @@ func (s *SnapshotStore) getIndex(ctx context.Context) (*storagefs.FliptIndex, er return idx, nil } + +func (s *SnapshotStore) GetVersion(ctx context.Context) (string, error) { + // TODO: implement + return "", nil +} diff --git a/internal/storage/fs/snapshot.go b/internal/storage/fs/snapshot.go index 43570229d0..bbfe0ed927 100644 --- a/internal/storage/fs/snapshot.go +++ b/internal/storage/fs/snapshot.go @@ -859,3 +859,8 @@ func (ss *Snapshot) getNamespace(key string) (namespace, error) { return *ns, nil } + +func (ss *Snapshot) GetVersion(context.Context, storage.NamespaceRequest) (string, error) { + // TODO: implement + return "", nil +} diff --git a/internal/storage/fs/store.go b/internal/storage/fs/store.go index 7d7ee21ac7..44729484b0 100644 --- a/internal/storage/fs/store.go +++ b/internal/storage/fs/store.go @@ -315,3 +315,8 @@ func (s *Store) DeleteRollout(ctx context.Context, r *flipt.DeleteRolloutRequest func (s *Store) OrderRollouts(ctx context.Context, r *flipt.OrderRolloutsRequest) error { return ErrNotImplemented } + +func (s *Store) GetVersion(context.Context, storage.NamespaceRequest) (string, error) { + // TODO: implement + return "", nil +} diff --git a/internal/storage/sql/common/flag.go b/internal/storage/sql/common/flag.go index 523996e357..f26248c3f6 100644 --- a/internal/storage/sql/common/flag.go +++ b/internal/storage/sql/common/flag.go @@ -314,7 +314,13 @@ func (s *Store) CountFlags(ctx context.Context, p storage.NamespaceRequest) (uin } // CreateFlag creates a flag -func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (*flipt.Flag, error) { +func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (_ *flipt.Flag, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -353,7 +359,13 @@ func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (*fl } // UpdateFlag updates an existing flag -func (s *Store) UpdateFlag(ctx context.Context, r *flipt.UpdateFlagRequest) (*flipt.Flag, error) { +func (s *Store) UpdateFlag(ctx context.Context, r *flipt.UpdateFlagRequest) (_ *flipt.Flag, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -386,6 +398,10 @@ func (s *Store) UpdateFlag(ctx context.Context, r *flipt.UpdateFlagRequest) (*fl // DeleteFlag deletes a flag func (s *Store) DeleteFlag(ctx context.Context, r *flipt.DeleteFlagRequest) error { + defer func() { + _ = s.setVersion(ctx, r.NamespaceKey) + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -399,6 +415,10 @@ func (s *Store) DeleteFlag(ctx context.Context, r *flipt.DeleteFlagRequest) erro // CreateVariant creates a variant func (s *Store) CreateVariant(ctx context.Context, r *flipt.CreateVariantRequest) (*flipt.Variant, error) { + defer func() { + _ = s.setVersion(ctx, r.NamespaceKey) + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -448,7 +468,13 @@ func (s *Store) CreateVariant(ctx context.Context, r *flipt.CreateVariantRequest } // UpdateVariant updates an existing variant -func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest) (*flipt.Variant, error) { +func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest) (_ *flipt.Variant, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -507,12 +533,18 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest } // DeleteVariant deletes a variant -func (s *Store) DeleteVariant(ctx context.Context, r *flipt.DeleteVariantRequest) error { +func (s *Store) DeleteVariant(ctx context.Context, r *flipt.DeleteVariantRequest) (err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } - _, err := s.builder.Delete("variants"). + _, err = s.builder.Delete("variants"). Where(sq.And{sq.Eq{"id": r.Id}, sq.Eq{"flag_key": r.FlagKey}, sq.Eq{"namespace_key": r.NamespaceKey}}). ExecContext(ctx) diff --git a/internal/storage/sql/common/namespace.go b/internal/storage/sql/common/namespace.go index d93328abd8..a27246bb17 100644 --- a/internal/storage/sql/common/namespace.go +++ b/internal/storage/sql/common/namespace.go @@ -155,7 +155,13 @@ func (s *Store) CountNamespaces(ctx context.Context, _ storage.ReferenceRequest) return count, nil } -func (s *Store) CreateNamespace(ctx context.Context, r *flipt.CreateNamespaceRequest) (*flipt.Namespace, error) { +func (s *Store) CreateNamespace(ctx context.Context, r *flipt.CreateNamespaceRequest) (_ *flipt.Namespace, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.Key) + } + }() + var ( now = flipt.Now() namespace = &flipt.Namespace{ @@ -183,7 +189,13 @@ func (s *Store) CreateNamespace(ctx context.Context, r *flipt.CreateNamespaceReq return namespace, nil } -func (s *Store) UpdateNamespace(ctx context.Context, r *flipt.UpdateNamespaceRequest) (*flipt.Namespace, error) { +func (s *Store) UpdateNamespace(ctx context.Context, r *flipt.UpdateNamespaceRequest) (_ *flipt.Namespace, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.Key) + } + }() + query := s.builder.Update("namespaces"). Set("name", r.Name). Set("description", r.Description). @@ -209,8 +221,9 @@ func (s *Store) UpdateNamespace(ctx context.Context, r *flipt.UpdateNamespaceReq return s.GetNamespace(ctx, p) } -func (s *Store) DeleteNamespace(ctx context.Context, r *flipt.DeleteNamespaceRequest) error { - _, err := s.builder.Delete("namespaces"). +func (s *Store) DeleteNamespace(ctx context.Context, r *flipt.DeleteNamespaceRequest) (err error) { + + _, err = s.builder.Delete("namespaces"). Where(sq.Eq{"\"key\"": r.Key}). ExecContext(ctx) diff --git a/internal/storage/sql/common/rollout.go b/internal/storage/sql/common/rollout.go index 90ace8cbf6..eb37dcd7ec 100644 --- a/internal/storage/sql/common/rollout.go +++ b/internal/storage/sql/common/rollout.go @@ -378,6 +378,12 @@ func (s *Store) CountRollouts(ctx context.Context, flag storage.ResourceRequest) } func (s *Store) CreateRollout(ctx context.Context, r *flipt.CreateRolloutRequest) (_ *flipt.Rollout, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -511,6 +517,12 @@ func (s *Store) CreateRollout(ctx context.Context, r *flipt.CreateRolloutRequest } func (s *Store) UpdateRollout(ctx context.Context, r *flipt.UpdateRolloutRequest) (_ *flipt.Rollout, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -663,6 +675,12 @@ func ensureRolloutType(rollout *flipt.Rollout, typ flipt.RolloutType) error { } func (s *Store) DeleteRollout(ctx context.Context, r *flipt.DeleteRolloutRequest) (err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -727,7 +745,13 @@ func (s *Store) DeleteRollout(ctx context.Context, r *flipt.DeleteRolloutRequest } // OrderRollouts orders rollouts -func (s *Store) OrderRollouts(ctx context.Context, r *flipt.OrderRolloutsRequest) error { +func (s *Store) OrderRollouts(ctx context.Context, r *flipt.OrderRolloutsRequest) (err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } diff --git a/internal/storage/sql/common/rule.go b/internal/storage/sql/common/rule.go index eb245a3e33..c6e0c0a202 100644 --- a/internal/storage/sql/common/rule.go +++ b/internal/storage/sql/common/rule.go @@ -346,6 +346,12 @@ func (s *Store) CountRules(ctx context.Context, flag storage.ResourceRequest) (u // CreateRule creates a rule func (s *Store) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest) (_ *flipt.Rule, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + segmentKeys := sanitizeSegmentKeys(r.GetSegmentKey(), r.GetSegmentKeys()) if r.NamespaceKey == "" { @@ -424,6 +430,12 @@ func (s *Store) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest) (_ * // UpdateRule updates an existing rule func (s *Store) UpdateRule(ctx context.Context, r *flipt.UpdateRuleRequest) (_ *flipt.Rule, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + segmentKeys := sanitizeSegmentKeys(r.GetSegmentKey(), r.GetSegmentKeys()) if r.NamespaceKey == "" { @@ -488,7 +500,13 @@ func (s *Store) UpdateRule(ctx context.Context, r *flipt.UpdateRuleRequest) (_ * } // DeleteRule deletes a rule -func (s *Store) DeleteRule(ctx context.Context, r *flipt.DeleteRuleRequest) error { +func (s *Store) DeleteRule(ctx context.Context, r *flipt.DeleteRuleRequest) (err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -554,7 +572,13 @@ func (s *Store) DeleteRule(ctx context.Context, r *flipt.DeleteRuleRequest) erro } // OrderRules orders rules -func (s *Store) OrderRules(ctx context.Context, r *flipt.OrderRulesRequest) error { +func (s *Store) OrderRules(ctx context.Context, r *flipt.OrderRulesRequest) (err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -620,7 +644,13 @@ func (s *Store) distributionValidationHelper(ctx context.Context, distributionRe } // CreateDistribution creates a distribution -func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistributionRequest) (*flipt.Distribution, error) { +func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistributionRequest) (_ *flipt.Distribution, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -637,7 +667,7 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut } ) - err := s.distributionValidationHelper(ctx, r) + err = s.distributionValidationHelper(ctx, r) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, errs.ErrNotFoundf("variant %q, rule %q, flag %q in namespace %q", r.VariantId, r.RuleId, r.FlagKey, r.NamespaceKey) @@ -645,7 +675,7 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut return nil, err } - if _, err := s.builder. + if _, err = s.builder. Insert("distributions"). Columns("id", "rule_id", "variant_id", "rollout", "created_at", "updated_at"). Values( @@ -663,13 +693,18 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut } // UpdateDistribution updates an existing distribution -// TODO: we dont let user to update variant_id currently.. we should -func (s *Store) UpdateDistribution(ctx context.Context, r *flipt.UpdateDistributionRequest) (*flipt.Distribution, error) { +func (s *Store) UpdateDistribution(ctx context.Context, r *flipt.UpdateDistributionRequest) (_ *flipt.Distribution, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } - err := s.distributionValidationHelper(ctx, r) + err = s.distributionValidationHelper(ctx, r) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, errs.ErrNotFoundf("variant %q, rule %q, flag %q in namespace %q", r.VariantId, r.RuleId, r.FlagKey, r.NamespaceKey) @@ -719,8 +754,14 @@ func (s *Store) UpdateDistribution(ctx context.Context, r *flipt.UpdateDistribut } // DeleteDistribution deletes a distribution -func (s *Store) DeleteDistribution(ctx context.Context, r *flipt.DeleteDistributionRequest) error { - _, err := s.builder.Delete("distributions"). +func (s *Store) DeleteDistribution(ctx context.Context, r *flipt.DeleteDistributionRequest) (err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + + _, err = s.builder.Delete("distributions"). Where(sq.And{sq.Eq{"id": r.Id}, sq.Eq{"rule_id": r.RuleId}, sq.Eq{"variant_id": r.VariantId}}). ExecContext(ctx) diff --git a/internal/storage/sql/common/segment.go b/internal/storage/sql/common/segment.go index 5b6bd350e5..afa719b1c2 100644 --- a/internal/storage/sql/common/segment.go +++ b/internal/storage/sql/common/segment.go @@ -294,7 +294,13 @@ func (s *Store) CountSegments(ctx context.Context, ns storage.NamespaceRequest) } // CreateSegment creates a segment -func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest) (*flipt.Segment, error) { +func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest) (_ *flipt.Segment, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -330,7 +336,13 @@ func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest } // UpdateSegment updates an existing segment -func (s *Store) UpdateSegment(ctx context.Context, r *flipt.UpdateSegmentRequest) (*flipt.Segment, error) { +func (s *Store) UpdateSegment(ctx context.Context, r *flipt.UpdateSegmentRequest) (_ *flipt.Segment, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -362,12 +374,18 @@ func (s *Store) UpdateSegment(ctx context.Context, r *flipt.UpdateSegmentRequest } // DeleteSegment deletes a segment -func (s *Store) DeleteSegment(ctx context.Context, r *flipt.DeleteSegmentRequest) error { +func (s *Store) DeleteSegment(ctx context.Context, r *flipt.DeleteSegmentRequest) (err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } - _, err := s.builder.Delete("segments"). + _, err = s.builder.Delete("segments"). Where(sq.And{sq.Eq{"namespace_key": r.NamespaceKey}, sq.Eq{"\"key\"": r.Key}}). ExecContext(ctx) @@ -375,7 +393,13 @@ func (s *Store) DeleteSegment(ctx context.Context, r *flipt.DeleteSegmentRequest } // CreateConstraint creates a constraint -func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintRequest) (*flipt.Constraint, error) { +func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintRequest) (_ *flipt.Constraint, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -423,7 +447,13 @@ func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintR } // UpdateConstraint updates an existing constraint -func (s *Store) UpdateConstraint(ctx context.Context, r *flipt.UpdateConstraintRequest) (*flipt.Constraint, error) { +func (s *Store) UpdateConstraint(ctx context.Context, r *flipt.UpdateConstraintRequest) (_ *flipt.Constraint, err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } @@ -482,12 +512,18 @@ func (s *Store) UpdateConstraint(ctx context.Context, r *flipt.UpdateConstraintR } // DeleteConstraint deletes a constraint -func (s *Store) DeleteConstraint(ctx context.Context, r *flipt.DeleteConstraintRequest) error { +func (s *Store) DeleteConstraint(ctx context.Context, r *flipt.DeleteConstraintRequest) (err error) { + defer func() { + if err == nil { + err = s.setVersion(ctx, r.NamespaceKey) + } + }() + if r.NamespaceKey == "" { r.NamespaceKey = storage.DefaultNamespace } - _, err := s.builder.Delete("constraints"). + _, err = s.builder.Delete("constraints"). Where(sq.And{sq.Eq{"id": r.Id}, sq.Eq{"segment_key": r.SegmentKey}, sq.Eq{"namespace_key": r.NamespaceKey}}). ExecContext(ctx) diff --git a/internal/storage/sql/common/storage.go b/internal/storage/sql/common/storage.go index b7bf37745c..f8c1f6ad09 100644 --- a/internal/storage/sql/common/storage.go +++ b/internal/storage/sql/common/storage.go @@ -1,10 +1,13 @@ package common import ( + "context" "database/sql" + "time" sq "github.com/Masterminds/squirrel" "go.flipt.io/flipt/internal/storage" + fliptsql "go.flipt.io/flipt/internal/storage/sql" "go.uber.org/zap" ) @@ -32,3 +35,35 @@ type PageToken struct { func (s *Store) String() string { return "" } + +func (s *Store) GetVersion(ctx context.Context, ns storage.NamespaceRequest) (string, error) { + var stateModifiedAt fliptsql.NullableTimestamp + + err := s.builder. + Select("state_modified_at"). + From("namespaces"). + Where(sq.Eq{"\"key\"": ns.Namespace()}). + Limit(1). + RunWith(s.db). + QueryRowContext(ctx). + Scan(&stateModifiedAt) + + if err != nil { + return "", err + } + + if !stateModifiedAt.IsValid() { + return "", nil + } + + return stateModifiedAt.Timestamp.String(), nil +} + +func (s *Store) setVersion(ctx context.Context, namespace string) error { + _, err := s.builder. + Update("namespaces"). + Set("state_modified_at", time.Now().UTC()). + Where(sq.Eq{"\"key\"": namespace}). + ExecContext(ctx) + return err +} diff --git a/internal/storage/sql/migrator.go b/internal/storage/sql/migrator.go index d4709eb893..dad7002a08 100644 --- a/internal/storage/sql/migrator.go +++ b/internal/storage/sql/migrator.go @@ -19,11 +19,11 @@ import ( ) var expectedVersions = map[Driver]uint{ - SQLite: 11, - LibSQL: 11, // libsql driver uses the same migrations as sqlite3 - Postgres: 12, - MySQL: 11, - CockroachDB: 9, + SQLite: 12, + LibSQL: 12, // libsql driver uses the same migrations as sqlite3 + Postgres: 13, + MySQL: 12, + CockroachDB: 10, Clickhouse: 3, } diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 81419e8e35..bab541d80a 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -153,6 +153,10 @@ func WithOrder(order Order) QueryOption { } } +type NamespaceVersionStore interface { + GetVersion(ctx context.Context, ns NamespaceRequest) (string, error) +} + // ReadOnlyStore is a storage implementation which only supports // reading the various types of state configuring within Flipt type ReadOnlyStore interface { @@ -162,6 +166,7 @@ type ReadOnlyStore interface { ReadOnlyRuleStore ReadOnlyRolloutStore EvaluationStore + NamespaceVersionStore fmt.Stringer } @@ -173,6 +178,7 @@ type Store interface { RuleStore RolloutStore EvaluationStore + NamespaceVersionStore fmt.Stringer } diff --git a/rpc/flipt/analytics/analytics.proto b/rpc/flipt/analytics/analytics.proto index 9560d90a97..0a841b30cd 100644 --- a/rpc/flipt/analytics/analytics.proto +++ b/rpc/flipt/analytics/analytics.proto @@ -16,6 +16,7 @@ message GetFlagEvaluationsCountResponse { repeated float values = 2; } +// flipt:sdk:ignore service AnalyticsService { rpc GetFlagEvaluationsCount(GetFlagEvaluationsCountRequest) returns (GetFlagEvaluationsCountResponse) {} } diff --git a/rpc/flipt/analytics/analytics_grpc.pb.go b/rpc/flipt/analytics/analytics_grpc.pb.go index 79b590f33e..dc986d932e 100644 --- a/rpc/flipt/analytics/analytics_grpc.pb.go +++ b/rpc/flipt/analytics/analytics_grpc.pb.go @@ -25,6 +25,8 @@ const ( // AnalyticsServiceClient is the client API for AnalyticsService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// flipt:sdk:ignore type AnalyticsServiceClient interface { GetFlagEvaluationsCount(ctx context.Context, in *GetFlagEvaluationsCountRequest, opts ...grpc.CallOption) (*GetFlagEvaluationsCountResponse, error) } @@ -50,6 +52,8 @@ func (c *analyticsServiceClient) GetFlagEvaluationsCount(ctx context.Context, in // AnalyticsServiceServer is the server API for AnalyticsService service. // All implementations must embed UnimplementedAnalyticsServiceServer // for forward compatibility +// +// flipt:sdk:ignore type AnalyticsServiceServer interface { GetFlagEvaluationsCount(context.Context, *GetFlagEvaluationsCountRequest) (*GetFlagEvaluationsCountResponse, error) mustEmbedUnimplementedAnalyticsServiceServer() diff --git a/sdk/go/grpc/grpc.sdk.gen.go b/sdk/go/grpc/grpc.sdk.gen.go index 1f3478ee11..96f7cd0644 100644 --- a/sdk/go/grpc/grpc.sdk.gen.go +++ b/sdk/go/grpc/grpc.sdk.gen.go @@ -4,7 +4,6 @@ package grpc import ( flipt "go.flipt.io/flipt/rpc/flipt" - analytics "go.flipt.io/flipt/rpc/flipt/analytics" auth "go.flipt.io/flipt/rpc/flipt/auth" evaluation "go.flipt.io/flipt/rpc/flipt/evaluation" meta "go.flipt.io/flipt/rpc/flipt/meta" @@ -22,10 +21,6 @@ func NewTransport(cc grpc.ClientConnInterface) Transport { return Transport{cc: cc} } -func (t Transport) AnalyticsClient() analytics.AnalyticsServiceClient { - return analytics.NewAnalyticsServiceClient(t.cc) -} - type authClient struct { cc grpc.ClientConnInterface } diff --git a/sdk/go/sdk.gen.go b/sdk/go/sdk.gen.go index 20553347d7..e14e2597f0 100644 --- a/sdk/go/sdk.gen.go +++ b/sdk/go/sdk.gen.go @@ -5,7 +5,6 @@ package sdk import ( context "context" flipt "go.flipt.io/flipt/rpc/flipt" - analytics "go.flipt.io/flipt/rpc/flipt/analytics" auth "go.flipt.io/flipt/rpc/flipt/auth" evaluation "go.flipt.io/flipt/rpc/flipt/evaluation" meta "go.flipt.io/flipt/rpc/flipt/meta" @@ -26,7 +25,6 @@ const ( ) type Transport interface { - AnalyticsClient() analytics.AnalyticsServiceClient AuthClient() AuthClient EvaluationClient() evaluation.EvaluationServiceClient FliptClient() flipt.FliptClient @@ -218,13 +216,6 @@ func New(t Transport, opts ...Option) SDK { return sdk } -func (s SDK) Analytics() *Analytics { - return &Analytics{ - transport: s.transport.AnalyticsClient(), - authenticationProvider: s.authenticationProvider, - } -} - func (s SDK) Auth() *Auth { return &Auth{ transport: s.transport.AuthClient(),