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: Add PostgreSQL Retriever #2790

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion cmd/relayproxy/config/retriever.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type RetrieverConf struct {
URI string `mapstructure:"uri" koanf:"uri"`
Database string `mapstructure:"database" koanf:"database"`
Collection string `mapstructure:"collection" koanf:"collection"`
Type string `mapstructure:"type" koanf:"type"`
Table string `mapstructure:"table" koanf:"table"`
Column string `mapstructure:"column" koanf:"column"`
RedisOptions *redis.Options `mapstructure:"redisOptions" koanf:"redisOptions"`
RedisPrefix string `mapstructure:"redisPrefix" koanf:"redisPrefix"`
AccountName string `mapstructure:"accountName" koanf:"accountname"`
Expand Down Expand Up @@ -67,6 +70,9 @@ func (c *RetrieverConf) IsValid() error {
if c.Kind == MongoDBRetriever {
return c.validateMongoDBRetriever()
}
if c.Kind == PostgreSQLRetriever {
return c.validatePostgreSQLRetriever()
}
if c.Kind == RedisRetriever {
return c.validateRedisRetriever()
}
Expand Down Expand Up @@ -112,6 +118,23 @@ func (c *RetrieverConf) validateMongoDBRetriever() error {
return nil
}

func (c *RetrieverConf) validatePostgreSQLRetriever() error {
if c.Column == "" {
return fmt.Errorf("invalid retriever: no \"column\" property found for kind \"%s\"", c.Kind)
}
if c.Table == "" {
return fmt.Errorf("invalid retriever: no \"table\" property found for kind \"%s\"", c.Kind)
}
if c.URI == "" {
return fmt.Errorf("invalid retriever: no \"uri\" property found for kind \"%s\"", c.Kind)
}
if c.Type == "" || !(c.Type == "json") {
return fmt.Errorf("invalid retriever: no \"type\" property or not a valid type in kind \"%s\"", c.Kind)
}

return nil
}

func (c *RetrieverConf) validateRedisRetriever() error {
if c.RedisOptions == nil {
return fmt.Errorf("invalid retriever: no \"redisOptions\" property found for kind \"%s\"", c.Kind)
Expand Down Expand Up @@ -144,6 +167,7 @@ const (
GoogleStorageRetriever RetrieverKind = "googleStorage"
KubernetesRetriever RetrieverKind = "configmap"
MongoDBRetriever RetrieverKind = "mongodb"
PostgreSQLRetriever RetrieverKind = "postgresql"
RedisRetriever RetrieverKind = "redis"
BitbucketRetriever RetrieverKind = "bitbucket"
AzBlobStorageRetriever RetrieverKind = "azureBlobStorage"
Expand All @@ -154,7 +178,7 @@ func (r RetrieverKind) IsValid() error {
switch r {
case HTTPRetriever, GitHubRetriever, GitlabRetriever, S3Retriever, RedisRetriever,
FileRetriever, GoogleStorageRetriever, KubernetesRetriever, MongoDBRetriever,
BitbucketRetriever, AzBlobStorageRetriever:
PostgreSQLRetriever, BitbucketRetriever, AzBlobStorageRetriever:
return nil
}
return fmt.Errorf("invalid retriever: kind \"%s\" is not supported", r)
Expand Down
67 changes: 67 additions & 0 deletions cmd/relayproxy/config/retriever_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,73 @@ func TestRetrieverConf_IsValid(t *testing.T) {
wantErr: true,
errValue: "invalid retriever: no \"redisOptions\" property found for kind \"redis\"",
},
{
name: "kind postgreSQL without Table",
fields: config.RetrieverConf{
Kind: "postgresql",
URI: "xxx",
Column: "xxx",
Type: "json",
},
wantErr: true,
errValue: "invalid retriever: no \"table\" property found for kind \"postgresql\"",
},
{
name: "kind postgreSQL without Column",
fields: config.RetrieverConf{
Kind: "postgresql",
URI: "xxx",
Table: "xxx",
Type: "json",
},
wantErr: true,
errValue: "invalid retriever: no \"column\" property found for kind \"postgresql\"",
},
{
name: "kind postgreSQL without URI",
fields: config.RetrieverConf{
Kind: "postgresql",
Column: "xxx",
Table: "xxx",
Type: "json",
},
wantErr: true,
errValue: "invalid retriever: no \"uri\" property found for kind \"postgresql\"",
},
{
name: "kind postgreSQL without Type",
fields: config.RetrieverConf{
Kind: "postgresql",
Column: "xxx",
Table: "xxx",
URI: "xxx",
},
wantErr: true,
errValue: "invalid retriever: no \"type\" property or not a valid type in kind \"postgresql\"",
},
{
name: "kind postgreSQL wrong Type",
fields: config.RetrieverConf{
Kind: "postgresql",
Column: "xxx",
Table: "xxx",
URI: "xxx",
Type: "wrong",
},
wantErr: true,
errValue: "invalid retriever: no \"type\" property or not a valid type in kind \"postgresql\"",
},

{
name: "kind postgreSQL valid",
fields: config.RetrieverConf{
Kind: "postgresql",
URI: "xxx",
Table: "xxx",
Type: "json",
Column: "xxx",
},
},
{
name: "kind mongoDB without Collection",
fields: config.RetrieverConf{
Expand Down
3 changes: 3 additions & 0 deletions cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"github.com/thomaspoignant/go-feature-flag/retriever/httpretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/k8sretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/mongodbretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/postgresqlretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/redisretriever"
"github.com/thomaspoignant/go-feature-flag/retriever/s3retrieverv2"
"go.uber.org/zap"
Expand Down Expand Up @@ -185,6 +186,8 @@
ClientConfig: *client}, nil
case config.MongoDBRetriever:
return &mongodbretriever.Retriever{Database: c.Database, URI: c.URI, Collection: c.Collection}, nil
case config.PostgreSQLRetriever:
return &postgresqlretriever.Retriever{Type: c.Type, URI: c.URI, Table: c.Table, Column: c.Column}, nil

Check warning on line 190 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L189-L190

Added lines #L189 - L190 were not covered by tests
case config.RedisRetriever:
return &redisretriever.Retriever{Options: c.RedisOptions, Prefix: c.RedisPrefix}, nil
case config.AzBlobStorageRetriever:
Expand Down
30 changes: 30 additions & 0 deletions examples/retriever_postgresql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# PostgreSQL example

This example contains everything you need to use **`PostgreSQL`** as the source for your flags.

As you can see the `main.go` file contains a basic HTTP server that expose an API that use your flags.

## How to setup the example
_All commands should be run in the root level of the repository._

1. Load all dependencies

```shell
make vendor
```

2. Run the PostgreSQL container provided in the `docker-compose.yml` file.

```shell
docker compose -f ./example/retriever_postgresql/docker-compose.yml up
```

The container will run an initialization script that populates the database with example flags

3. Run the example app to visualize the flags being evaluated

```shell
go run ./examples/retriever_postgresql/main.go
```

4. Play with the values in the configured MongoDB documents to see different outputs
17 changes: 17 additions & 0 deletions examples/retriever_postgresql/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Use root/example as user/password credentials
version: '3.1'

services:

postgres:
container_name: goff_postgres
hostname: postgres
image: postgres:16-alpine
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=example
- POSTGRES_DB=flags_db
ports:
- "5432:5432"
volumes:
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
53 changes: 53 additions & 0 deletions examples/retriever_postgresql/init-db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash

set -e

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE TABLE IF NOT EXISTS flags (
id SERIAL PRIMARY KEY,
flag JSONB
);

INSERT INTO flags (flag) VALUES
(
'{
"flag": "new-admin-access",
"variations": {
"default_var": false,
"false_var": false,
"true_var": true
},
"defaultRule": {
"percentage": {
"false_var": 70,
"true_var": 30
}
}
}'::jsonb
);

INSERT INTO flags (flag) VALUES
(
'{
"flag": "flag-only-for-admin",
"variations": {
"default_var": false,
"false_var": false,
"true_var": true
},
"targeting": [
{
"query": "admin eq true",
"percentage": {
"false_var": 0,
"true_var": 100
}
}
],
"defaultRule": {
"variation": "default_var"
}
}'::jsonb
);
EOSQL

91 changes: 91 additions & 0 deletions examples/retriever_postgresql/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"context"
"fmt"
"log"
"log/slog"
"time"

"github.com/thomaspoignant/go-feature-flag/ffcontext"

"github.com/thomaspoignant/go-feature-flag/exporter/fileexporter"
"github.com/thomaspoignant/go-feature-flag/retriever/postgresqlretriever"

ffclient "github.com/thomaspoignant/go-feature-flag"
)

func main() {
// Init ffclient with a file retriever.
err := ffclient.Init(ffclient.Config{
PollingInterval: 10 * time.Second,
LeveledLogger: slog.Default(),
Context: context.Background(),
Retriever: &postgresqlretriever.Retriever{
Table: "flags",
Type: "json",
Column: "flag",
URI: "postgres://root:example@localhost:5432/flags_db?sslmode=disable",
},
DataExporter: ffclient.DataExporter{
FlushInterval: 1 * time.Second,
MaxEventInMemory: 100,
Exporter: &fileexporter.Exporter{
OutputDir: "./",
},
},
})
// Check init errors.
if err != nil {
log.Fatal(err)
}
// defer closing ffclient
defer ffclient.Close()

// create users
user1 := ffcontext.
NewEvaluationContextBuilder("aea2fdc1-b9a0-417a-b707-0c9083de68e3").
AddCustom("anonymous", true).
AddCustom("environment", "dev").
Build()
user2 := ffcontext.NewEvaluationContext("332460b9-a8aa-4f7a-bc5d-9cc33632df9a")
user3 := ffcontext.NewEvaluationContextBuilder("785a14bf-d2c5-4caa-9c70-2bbc4e3732a5").
AddCustom("email", "[email protected]").
AddCustom("firstname", "John").
AddCustom("lastname", "Doe").
AddCustom("admin", true).
Build()

// --- test flag with no rule
// user1
user1HasAccessToNewAdmin, err := ffclient.BoolVariation("new-admin-access", user1, false)
if err != nil {
// we log the error, but we still have a meaningful value in user1HasAccessToNewAdmin (the default value).
log.Printf("something went wrong when getting the flag: %v", err)
}
if user1HasAccessToNewAdmin {
fmt.Println("user1 has access to the new admin")
}

// user2
user2HasAccessToNewAdmin, err := ffclient.BoolVariation("new-admin-access", user2, false)
if err != nil {
// we log the error, but we still have a meaningful value in hasAccessToNewAdmin (the default value).
log.Printf("something went wrong when getting the flag: %v", err)
}
if !user2HasAccessToNewAdmin {
fmt.Println("user2 has not access to the new admin")
}

// --- test flag with rule only for admins
// user 1 is not admin so should not access to the flag
user1HasAccess, _ := ffclient.BoolVariation("flag-only-for-admin", user1, false)
if !user1HasAccess {
fmt.Println("user1 is not admin so no access to the flag")
}

// user 3 is admin and the flag apply to this key.
if user3HasAccess, _ := ffclient.BoolVariation("flag-only-for-admin", user3, false); user3HasAccess {
fmt.Println("user 3 is admin and the flag apply to this key.")
}
}
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/invopop/jsonschema v0.12.0
github.com/jackc/pgx/v5 v5.5.4
github.com/jessevdk/go-flags v1.6.1
github.com/jsternberg/zap-logfmt v1.3.0
github.com/knadh/koanf/parsers/json v0.1.0
Expand All @@ -39,6 +40,7 @@ require (
github.com/knadh/koanf/v2 v2.1.2
github.com/labstack/echo-contrib v0.17.1
github.com/labstack/echo/v4 v4.13.0
github.com/lib/pq v1.10.9
github.com/luci/go-render v0.0.0-20160219211803-9a04cc21af0f
github.com/nikunjy/rules v1.5.0
github.com/pablor21/echo-etag/v4 v4.0.3
Expand All @@ -54,6 +56,7 @@ require (
github.com/testcontainers/testcontainers-go v0.34.0
github.com/testcontainers/testcontainers-go/modules/azurite v0.34.0
github.com/testcontainers/testcontainers-go/modules/mongodb v0.34.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.34.0
github.com/testcontainers/testcontainers-go/modules/redis v0.34.0
github.com/thejerf/slogassert v0.3.4
github.com/xitongsys/parquet-go v1.6.2
Expand Down Expand Up @@ -164,6 +167,9 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
Expand Down
Loading
Loading