feat: initial commit
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
This commit is contained in:
8
internal/store/error.go
Normal file
8
internal/store/error.go
Normal file
@ -0,0 +1,8 @@
|
||||
package store
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrAlreadyExist = errors.New("already exist")
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
25
internal/store/layer.go
Normal file
25
internal/store/layer.go
Normal file
@ -0,0 +1,25 @@
|
||||
package store
|
||||
|
||||
import "time"
|
||||
|
||||
type (
|
||||
LayerName Name
|
||||
LayerType string
|
||||
)
|
||||
|
||||
type LayerHeader struct {
|
||||
Proxy ProxyName `json:"proxy"`
|
||||
Name LayerName `json:"name"`
|
||||
Type LayerType `json:"type"`
|
||||
|
||||
Weight int `json:"weight"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type Layer struct {
|
||||
LayerHeader
|
||||
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Options LayerOptions `json:"options"`
|
||||
}
|
78
internal/store/layer_repository.go
Normal file
78
internal/store/layer_repository.go
Normal file
@ -0,0 +1,78 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type LayerOptions map[string]any
|
||||
|
||||
type LayerRepository interface {
|
||||
CreateLayer(ctx context.Context, proxyName ProxyName, layerName LayerName, layerType LayerType, options LayerOptions) (*Layer, error)
|
||||
UpdateLayer(ctx context.Context, proxyName ProxyName, layerName LayerName, funcs ...UpdateLayerOptionFunc) (*Layer, error)
|
||||
DeleteLayer(ctx context.Context, proxyName ProxyName, layerName LayerName) error
|
||||
GetLayer(ctx context.Context, proxyName ProxyName, layerName LayerName) (*Layer, error)
|
||||
QueryLayers(ctx context.Context, proxyName ProxyName, funcs ...QueryLayerOptionFunc) ([]*LayerHeader, error)
|
||||
}
|
||||
|
||||
type QueryLayerOptionFunc func(*QueryLayerOptions)
|
||||
|
||||
type QueryLayerOptions struct {
|
||||
Type *LayerType
|
||||
Name *LayerName
|
||||
Enabled *bool
|
||||
}
|
||||
|
||||
func DefaultQueryLayerOptions() *QueryLayerOptions {
|
||||
funcs := []QueryLayerOptionFunc{}
|
||||
|
||||
opts := &QueryLayerOptions{}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithLayerQueryType(layerType LayerType) QueryLayerOptionFunc {
|
||||
return func(o *QueryLayerOptions) {
|
||||
o.Type = &layerType
|
||||
}
|
||||
}
|
||||
|
||||
func WithLayerQueryName(layerName LayerName) QueryLayerOptionFunc {
|
||||
return func(o *QueryLayerOptions) {
|
||||
o.Name = &layerName
|
||||
}
|
||||
}
|
||||
|
||||
func WithLayerQueryEnabled(enabled bool) QueryLayerOptionFunc {
|
||||
return func(o *QueryLayerOptions) {
|
||||
o.Enabled = &enabled
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateLayerOptionFunc func(*UpdateLayerOptions)
|
||||
|
||||
type UpdateLayerOptions struct {
|
||||
Enabled *bool
|
||||
Weight *int
|
||||
Options *LayerOptions
|
||||
}
|
||||
|
||||
func WithLayerUpdateEnabled(enabled bool) UpdateLayerOptionFunc {
|
||||
return func(o *UpdateLayerOptions) {
|
||||
o.Enabled = &enabled
|
||||
}
|
||||
}
|
||||
|
||||
func WithLayerUpdateWeight(weight int) UpdateLayerOptionFunc {
|
||||
return func(o *UpdateLayerOptions) {
|
||||
o.Weight = &weight
|
||||
}
|
||||
}
|
||||
|
||||
func WithLayerUpdateOptions(options LayerOptions) UpdateLayerOptionFunc {
|
||||
return func(o *UpdateLayerOptions) {
|
||||
o.Options = &options
|
||||
}
|
||||
}
|
14
internal/store/name.go
Normal file
14
internal/store/name.go
Normal file
@ -0,0 +1,14 @@
|
||||
package store
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
type Name string
|
||||
|
||||
var ErrEmptyName = errors.New("name cannot be empty")
|
||||
|
||||
func ValidateName(name string) (Name, error) {
|
||||
if name == "" {
|
||||
return "", errors.WithStack(ErrEmptyName)
|
||||
}
|
||||
return Name(name), nil
|
||||
}
|
22
internal/store/proxy.go
Normal file
22
internal/store/proxy.go
Normal file
@ -0,0 +1,22 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProxyName Name
|
||||
|
||||
type ProxyHeader struct {
|
||||
Name ProxyName `json:"name"`
|
||||
|
||||
Weight int `json:"weight"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
ProxyHeader
|
||||
To string `json:"to"`
|
||||
From []string `json:"from"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
91
internal/store/proxy_repository.go
Normal file
91
internal/store/proxy_repository.go
Normal file
@ -0,0 +1,91 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type ProxyRepository interface {
|
||||
CreateProxy(ctx context.Context, name ProxyName, to string, from ...string) (*Proxy, error)
|
||||
UpdateProxy(ctx context.Context, name ProxyName, funcs ...UpdateProxyOptionFunc) (*Proxy, error)
|
||||
QueryProxy(ctx context.Context, funcs ...QueryProxyOptionFunc) ([]*ProxyHeader, error)
|
||||
GetProxy(ctx context.Context, name ProxyName) (*Proxy, error)
|
||||
DeleteProxy(ctx context.Context, name ProxyName) error
|
||||
}
|
||||
|
||||
type UpdateProxyOptionFunc func(*UpdateProxyOptions)
|
||||
|
||||
type UpdateProxyOptions struct {
|
||||
To *string
|
||||
From []string
|
||||
Enabled *bool
|
||||
Weight *int
|
||||
}
|
||||
|
||||
func WithProxyUpdateEnabled(enabled bool) UpdateProxyOptionFunc {
|
||||
return func(o *UpdateProxyOptions) {
|
||||
o.Enabled = &enabled
|
||||
}
|
||||
}
|
||||
|
||||
func WithProxyUpdateWeight(weight int) UpdateProxyOptionFunc {
|
||||
return func(o *UpdateProxyOptions) {
|
||||
o.Weight = &weight
|
||||
}
|
||||
}
|
||||
|
||||
func WithProxyUpdateTo(to string) UpdateProxyOptionFunc {
|
||||
return func(o *UpdateProxyOptions) {
|
||||
o.To = &to
|
||||
}
|
||||
}
|
||||
|
||||
func WithProxyUpdateFrom(from ...string) UpdateProxyOptionFunc {
|
||||
return func(o *UpdateProxyOptions) {
|
||||
o.From = from
|
||||
}
|
||||
}
|
||||
|
||||
type QueryProxyOptionFunc func(*QueryProxyOptions)
|
||||
|
||||
type QueryProxyOptions struct {
|
||||
To *url.URL
|
||||
Names []ProxyName
|
||||
Enabled *bool
|
||||
From []string
|
||||
}
|
||||
|
||||
func DefaultQueryProxyOptions() *QueryProxyOptions {
|
||||
funcs := []QueryProxyOptionFunc{}
|
||||
|
||||
opts := &QueryProxyOptions{}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithProxyQueryTo(to *url.URL) QueryProxyOptionFunc {
|
||||
return func(o *QueryProxyOptions) {
|
||||
o.To = to
|
||||
}
|
||||
}
|
||||
|
||||
func WithProxyQueryFrom(from ...string) QueryProxyOptionFunc {
|
||||
return func(o *QueryProxyOptions) {
|
||||
o.From = from
|
||||
}
|
||||
}
|
||||
|
||||
func WithProxyQueryNames(names ...ProxyName) QueryProxyOptionFunc {
|
||||
return func(o *QueryProxyOptions) {
|
||||
o.Names = names
|
||||
}
|
||||
}
|
||||
|
||||
func WithProxyQueryEnabled(enabled bool) QueryProxyOptionFunc {
|
||||
return func(o *QueryProxyOptions) {
|
||||
o.Enabled = &enabled
|
||||
}
|
||||
}
|
93
internal/store/redis/helper.go
Normal file
93
internal/store/redis/helper.go
Normal file
@ -0,0 +1,93 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type jsonWrapper[T any] struct {
|
||||
value T
|
||||
}
|
||||
|
||||
func (w *jsonWrapper[T]) MarshalBinary() ([]byte, error) {
|
||||
data, err := json.Marshal(w.value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (w *jsonWrapper[T]) UnmarshalBinary(data []byte) error {
|
||||
if err := json.Unmarshal(data, &w.value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *jsonWrapper[T]) UnmarshalText(data []byte) error {
|
||||
if err := json.Unmarshal(data, &w.value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *jsonWrapper[T]) Value() T {
|
||||
return w.value
|
||||
}
|
||||
|
||||
func wrap[T any](v T) *jsonWrapper[T] {
|
||||
return &jsonWrapper[T]{v}
|
||||
}
|
||||
|
||||
func unwrap[T any](v any) (T, error) {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return *new(T), errors.Errorf("could not unwrap value of type '%T'", v)
|
||||
}
|
||||
|
||||
u := new(T)
|
||||
|
||||
if err := json.Unmarshal([]byte(str), u); err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
return *u, nil
|
||||
}
|
||||
|
||||
func key(parts ...string) string {
|
||||
return strings.Join(parts, ":")
|
||||
}
|
||||
|
||||
func WithTx(ctx context.Context, client redis.UniversalClient, key string, fn func(ctx context.Context, tx *redis.Tx) error) error {
|
||||
txf := func(tx *redis.Tx) error {
|
||||
if err := fn(ctx, tx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := client.Watch(ctx, txf, key)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains[T ~string](values []T, v T) bool {
|
||||
for _, vv := range values {
|
||||
if vv == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
62
internal/store/redis/layer_item.go
Normal file
62
internal/store/redis/layer_item.go
Normal file
@ -0,0 +1,62 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type layerHeaderItem struct {
|
||||
Proxy string `redis:"proxy"`
|
||||
Name string `redis:"name"`
|
||||
Type string `redis:"type"`
|
||||
|
||||
Weight int `redis:"weight"`
|
||||
Enabled bool `redis:"enabled"`
|
||||
}
|
||||
|
||||
func (i *layerHeaderItem) ToLayerHeader() (*store.LayerHeader, error) {
|
||||
layerHeader := &store.LayerHeader{
|
||||
Proxy: store.ProxyName(i.Proxy),
|
||||
Name: store.LayerName(i.Name),
|
||||
Type: store.LayerType(i.Type),
|
||||
Weight: i.Weight,
|
||||
Enabled: i.Enabled,
|
||||
}
|
||||
|
||||
return layerHeader, nil
|
||||
}
|
||||
|
||||
type layerItem struct {
|
||||
layerHeaderItem
|
||||
Options *jsonWrapper[store.LayerOptions] `redis:"options"`
|
||||
|
||||
CreatedAt *jsonWrapper[time.Time] `redis:"created_at"`
|
||||
UpdatedAt *jsonWrapper[time.Time] `redis:"updated_at"`
|
||||
}
|
||||
|
||||
func (i *layerItem) ToLayer() (*store.Layer, error) {
|
||||
layerHeader, err := i.layerHeaderItem.ToLayerHeader()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
layer := &store.Layer{
|
||||
LayerHeader: *layerHeader,
|
||||
}
|
||||
|
||||
if i.Options != nil {
|
||||
layer.Options = i.Options.Value()
|
||||
}
|
||||
|
||||
if i.CreatedAt != nil {
|
||||
layer.CreatedAt = i.CreatedAt.Value()
|
||||
}
|
||||
|
||||
if i.UpdatedAt != nil {
|
||||
layer.UpdatedAt = i.UpdatedAt.Value()
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
256
internal/store/redis/layer_repository.go
Normal file
256
internal/store/redis/layer_repository.go
Normal file
@ -0,0 +1,256 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
keyPrefixLayer = "layer"
|
||||
)
|
||||
|
||||
type LayerRepository struct {
|
||||
client redis.UniversalClient
|
||||
}
|
||||
|
||||
// CreateLayer implements store.LayerRepository
|
||||
func (r *LayerRepository) CreateLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, layerType store.LayerType, options store.LayerOptions) (*store.Layer, error) {
|
||||
now := time.Now().UTC()
|
||||
key := layerKey(proxyName, layerName)
|
||||
|
||||
layerItem := &layerItem{
|
||||
layerHeaderItem: layerHeaderItem{
|
||||
Proxy: string(proxyName),
|
||||
Name: string(layerName),
|
||||
Type: string(layerType),
|
||||
Weight: 0,
|
||||
Enabled: false,
|
||||
},
|
||||
|
||||
CreatedAt: wrap(now),
|
||||
UpdatedAt: wrap(now),
|
||||
Options: wrap(store.LayerOptions{}),
|
||||
}
|
||||
|
||||
txf := func(tx *redis.Tx) error {
|
||||
exists, err := tx.Exists(ctx, key).Uint64()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if exists > 0 {
|
||||
return errors.WithStack(store.ErrAlreadyExist)
|
||||
}
|
||||
|
||||
_, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
|
||||
p.HMSet(ctx, key, &layerItem.layerHeaderItem)
|
||||
p.HMSet(ctx, key, layerItem)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := r.client.Watch(ctx, txf, key)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &store.Layer{
|
||||
LayerHeader: store.LayerHeader{
|
||||
Name: layerName,
|
||||
Proxy: proxyName,
|
||||
Type: layerType,
|
||||
Weight: 0,
|
||||
Enabled: false,
|
||||
},
|
||||
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Options: store.LayerOptions{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteLayer implements store.LayerRepository
|
||||
func (r *LayerRepository) DeleteLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName) error {
|
||||
key := layerKey(proxyName, layerName)
|
||||
|
||||
if cmd := r.client.Del(ctx, key); cmd.Err() != nil {
|
||||
return errors.WithStack(cmd.Err())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLayer implements store.LayerRepository
|
||||
func (r *LayerRepository) GetLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName) (*store.Layer, error) {
|
||||
key := layerKey(proxyName, layerName)
|
||||
var layerItem *layerItem
|
||||
|
||||
err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error {
|
||||
pItem, err := r.txGetLayerItem(ctx, tx, proxyName, layerName)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
layerItem = pItem
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
layer, err := layerItem.ToLayer()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
func (r *LayerRepository) txGetLayerItem(ctx context.Context, tx *redis.Tx, proxyName store.ProxyName, layerName store.LayerName) (*layerItem, error) {
|
||||
layerItem := layerItem{}
|
||||
key := layerKey(proxyName, layerName)
|
||||
|
||||
exists, err := tx.Exists(ctx, key).Uint64()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if exists == 0 {
|
||||
return nil, errors.WithStack(store.ErrNotFound)
|
||||
}
|
||||
|
||||
if err := tx.HGetAll(ctx, key).Scan(&layerItem.layerHeaderItem); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := tx.HGetAll(ctx, key).Scan(&layerItem); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &layerItem, nil
|
||||
}
|
||||
|
||||
// QueryLayers implements store.LayerRepository
|
||||
func (r *LayerRepository) QueryLayers(ctx context.Context, proxyName store.ProxyName, funcs ...store.QueryLayerOptionFunc) ([]*store.LayerHeader, error) {
|
||||
opts := store.DefaultQueryLayerOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
keyParts := []string{keyPrefixLayer, string(proxyName)}
|
||||
|
||||
if opts.Name != nil {
|
||||
keyParts = append(keyParts, string(*opts.Name))
|
||||
} else {
|
||||
keyParts = append(keyParts, "*")
|
||||
}
|
||||
|
||||
key := key(keyParts...)
|
||||
|
||||
iter := r.client.Scan(ctx, 0, key, 0).Iterator()
|
||||
|
||||
headers := make([]*store.LayerHeader, 0)
|
||||
|
||||
for iter.Next(ctx) {
|
||||
key := iter.Val()
|
||||
|
||||
layerHeaderItem := &layerHeaderItem{}
|
||||
|
||||
if err := r.client.HGetAll(ctx, key).Scan(layerHeaderItem); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
layerHeader, err := layerHeaderItem.ToLayerHeader()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
headers = append(headers, layerHeader)
|
||||
}
|
||||
|
||||
if err := iter.Err(); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
// UpdateLayer implements store.LayerRepository
|
||||
func (r *LayerRepository) UpdateLayer(ctx context.Context, proxyName store.ProxyName, layerName store.LayerName, funcs ...store.UpdateLayerOptionFunc) (*store.Layer, error) {
|
||||
opts := &store.UpdateLayerOptions{}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
key := layerKey(proxyName, layerName)
|
||||
var layerItem layerItem
|
||||
|
||||
err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error {
|
||||
item, err := r.txGetLayerItem(ctx, tx, proxyName, layerName)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if opts.Enabled != nil {
|
||||
item.Enabled = *opts.Enabled
|
||||
}
|
||||
|
||||
if opts.Weight != nil {
|
||||
item.Weight = *opts.Weight
|
||||
}
|
||||
|
||||
if opts.Options != nil {
|
||||
item.Options = wrap(*opts.Options)
|
||||
}
|
||||
|
||||
item.UpdatedAt = wrap(time.Now().UTC())
|
||||
|
||||
_, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
|
||||
p.HMSet(ctx, key, item.layerHeaderItem)
|
||||
p.HMSet(ctx, key, item)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
layerItem = *item
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
layer, err := layerItem.ToLayer()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
func NewLayerRepository(client redis.UniversalClient) *LayerRepository {
|
||||
return &LayerRepository{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
var _ store.LayerRepository = &LayerRepository{}
|
||||
|
||||
func layerKey(proxyName store.ProxyName, layerName store.LayerName) string {
|
||||
return key(keyPrefixLayer, string(proxyName), string(layerName))
|
||||
}
|
12
internal/store/redis/layer_repository_test.go
Normal file
12
internal/store/redis/layer_repository_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store/testsuite"
|
||||
)
|
||||
|
||||
func TestLayerRepository(t *testing.T) {
|
||||
repository := NewLayerRepository(client)
|
||||
testsuite.TestLayerRepository(t, repository)
|
||||
}
|
58
internal/store/redis/main_test.go
Normal file
58
internal/store/redis/main_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var client redis.UniversalClient
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
// uses pool to try to connect to Docker
|
||||
err = pool.Client.Ping()
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
// pulls an image, creates a container based on it and runs it
|
||||
resource, err := pool.Run("redis", "alpine3.17", []string{})
|
||||
if err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
client = redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
Addrs: []string{resource.GetHostPort("6379/tcp")},
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if cmd := client.Ping(ctx); cmd.Err() != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(resource); err != nil {
|
||||
log.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
60
internal/store/redis/proxy_item.go
Normal file
60
internal/store/redis/proxy_item.go
Normal file
@ -0,0 +1,60 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type proxyHeaderItem struct {
|
||||
Name string `redis:"name"`
|
||||
|
||||
Weight int `redis:"weight"`
|
||||
Enabled bool `redis:"enabled"`
|
||||
|
||||
CreatedAt *jsonWrapper[time.Time] `redis:"created_at"`
|
||||
UpdatedAt *jsonWrapper[time.Time] `redis:"updated_at"`
|
||||
}
|
||||
|
||||
func (i *proxyHeaderItem) ToProxyHeader() (*store.ProxyHeader, error) {
|
||||
proxyHeader := &store.ProxyHeader{
|
||||
Name: store.ProxyName(i.Name),
|
||||
Weight: i.Weight,
|
||||
Enabled: i.Enabled,
|
||||
}
|
||||
|
||||
return proxyHeader, nil
|
||||
}
|
||||
|
||||
type proxyItem struct {
|
||||
proxyHeaderItem
|
||||
To string `redis:"to"`
|
||||
From *jsonWrapper[[]string] `redis:"from"`
|
||||
}
|
||||
|
||||
func (i *proxyItem) ToProxy() (*store.Proxy, error) {
|
||||
proxyHeader, err := i.proxyHeaderItem.ToProxyHeader()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
proxy := &store.Proxy{
|
||||
ProxyHeader: *proxyHeader,
|
||||
To: i.To,
|
||||
}
|
||||
|
||||
if i.CreatedAt != nil {
|
||||
proxy.CreatedAt = i.CreatedAt.Value()
|
||||
}
|
||||
|
||||
if i.UpdatedAt != nil {
|
||||
proxy.UpdatedAt = i.UpdatedAt.Value()
|
||||
}
|
||||
|
||||
if i.From != nil {
|
||||
proxy.From = i.From.Value()
|
||||
}
|
||||
|
||||
return proxy, nil
|
||||
}
|
254
internal/store/redis/proxy_repository.go
Normal file
254
internal/store/redis/proxy_repository.go
Normal file
@ -0,0 +1,254 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
keyPrefixProxy = "proxy"
|
||||
)
|
||||
|
||||
type ProxyRepository struct {
|
||||
client redis.UniversalClient
|
||||
}
|
||||
|
||||
// GetProxy implements store.ProxyRepository
|
||||
func (r *ProxyRepository) GetProxy(ctx context.Context, name store.ProxyName) (*store.Proxy, error) {
|
||||
key := proxyKey(name)
|
||||
var proxyItem *proxyItem
|
||||
|
||||
err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error {
|
||||
pItem, err := r.txGetProxyItem(ctx, tx, name)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
proxyItem = pItem
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
proxy, err := proxyItem.ToProxy()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func (r *ProxyRepository) txGetProxyItem(ctx context.Context, tx *redis.Tx, name store.ProxyName) (*proxyItem, error) {
|
||||
proxyItem := proxyItem{}
|
||||
key := proxyKey(name)
|
||||
|
||||
exists, err := tx.Exists(ctx, key).Uint64()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if exists == 0 {
|
||||
return nil, errors.WithStack(store.ErrNotFound)
|
||||
}
|
||||
|
||||
if err := tx.HGetAll(ctx, key).Scan(&proxyItem.proxyHeaderItem); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := tx.HGetAll(ctx, key).Scan(&proxyItem); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &proxyItem, nil
|
||||
}
|
||||
|
||||
// CreateProxy implements store.ProxyRepository
|
||||
func (r *ProxyRepository) CreateProxy(ctx context.Context, name store.ProxyName, to string, from ...string) (*store.Proxy, error) {
|
||||
now := time.Now().UTC()
|
||||
key := proxyKey(name)
|
||||
|
||||
txf := func(tx *redis.Tx) error {
|
||||
exists, err := tx.Exists(ctx, key).Uint64()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if exists > 0 {
|
||||
return errors.WithStack(store.ErrAlreadyExist)
|
||||
}
|
||||
|
||||
proxyItem := &proxyItem{
|
||||
proxyHeaderItem: proxyHeaderItem{
|
||||
Name: string(name),
|
||||
CreatedAt: wrap(now),
|
||||
UpdatedAt: wrap(now),
|
||||
Weight: 0,
|
||||
Enabled: false,
|
||||
},
|
||||
To: to,
|
||||
From: wrap(from),
|
||||
}
|
||||
|
||||
_, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
|
||||
p.HMSet(ctx, key, proxyItem.proxyHeaderItem)
|
||||
p.HMSet(ctx, key, proxyItem)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := r.client.Watch(ctx, txf, key)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &store.Proxy{
|
||||
ProxyHeader: store.ProxyHeader{
|
||||
Name: name,
|
||||
|
||||
Weight: 0,
|
||||
Enabled: false,
|
||||
},
|
||||
To: to,
|
||||
From: from,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteProxy implements store.ProxyRepository
|
||||
func (r *ProxyRepository) DeleteProxy(ctx context.Context, name store.ProxyName) error {
|
||||
key := proxyKey(name)
|
||||
|
||||
if cmd := r.client.Del(ctx, key); cmd.Err() != nil {
|
||||
return errors.WithStack(cmd.Err())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryProxy implements store.ProxyRepository
|
||||
func (r *ProxyRepository) QueryProxy(ctx context.Context, funcs ...store.QueryProxyOptionFunc) ([]*store.ProxyHeader, error) {
|
||||
opts := store.DefaultQueryProxyOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
iter := r.client.Scan(ctx, 0, keyPrefixProxy+"*", 0).Iterator()
|
||||
|
||||
headers := make([]*store.ProxyHeader, 0)
|
||||
|
||||
for iter.Next(ctx) {
|
||||
key := iter.Val()
|
||||
|
||||
proxyHeaderItem := &proxyHeaderItem{}
|
||||
if err := r.client.HGetAll(ctx, key).Scan(proxyHeaderItem); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
proxyHeader, err := proxyHeaderItem.ToProxyHeader()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if opts.Enabled != nil && proxyHeader.Enabled != *opts.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.Names != nil && !contains(opts.Names, proxyHeader.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
headers = append(headers, proxyHeader)
|
||||
}
|
||||
|
||||
if err := iter.Err(); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
// UpdateProxy implements store.ProxyRepository
|
||||
func (r *ProxyRepository) UpdateProxy(ctx context.Context, name store.ProxyName, funcs ...store.UpdateProxyOptionFunc) (*store.Proxy, error) {
|
||||
opts := &store.UpdateProxyOptions{}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
key := proxyKey(name)
|
||||
var proxyItem proxyItem
|
||||
|
||||
err := WithTx(ctx, r.client, key, func(ctx context.Context, tx *redis.Tx) error {
|
||||
item, err := r.txGetProxyItem(ctx, tx, name)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if opts.Enabled != nil {
|
||||
item.Enabled = *opts.Enabled
|
||||
}
|
||||
|
||||
if opts.From != nil {
|
||||
item.From = wrap(opts.From)
|
||||
}
|
||||
|
||||
if opts.Weight != nil {
|
||||
item.Weight = *opts.Weight
|
||||
}
|
||||
|
||||
if opts.To != nil {
|
||||
item.To = *opts.To
|
||||
}
|
||||
|
||||
item.UpdatedAt = wrap(time.Now().UTC())
|
||||
|
||||
_, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
|
||||
p.HMSet(ctx, key, item.proxyHeaderItem)
|
||||
p.HMSet(ctx, key, item)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
proxyItem = *item
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
proxy, err := proxyItem.ToProxy()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func NewProxyRepository(client redis.UniversalClient) *ProxyRepository {
|
||||
return &ProxyRepository{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
var _ store.ProxyRepository = &ProxyRepository{}
|
||||
|
||||
func proxyKey(name store.ProxyName) string {
|
||||
return key(keyPrefixProxy, string(name))
|
||||
}
|
12
internal/store/redis/proxy_repository_test.go
Normal file
12
internal/store/redis/proxy_repository_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store/testsuite"
|
||||
)
|
||||
|
||||
func TestProxyRepository(t *testing.T) {
|
||||
repository := NewProxyRepository(client)
|
||||
testsuite.TestProxyRepository(t, repository)
|
||||
}
|
13
internal/store/sort.go
Normal file
13
internal/store/sort.go
Normal file
@ -0,0 +1,13 @@
|
||||
package store
|
||||
|
||||
type ByProxyWeight []*ProxyHeader
|
||||
|
||||
func (s ByProxyWeight) Len() int { return len(s) }
|
||||
func (s ByProxyWeight) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s ByProxyWeight) Less(i, j int) bool { return s[i].Weight > s[j].Weight }
|
||||
|
||||
type ByLayerWeight []*LayerHeader
|
||||
|
||||
func (s ByLayerWeight) Len() int { return len(s) }
|
||||
func (s ByLayerWeight) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s ByLayerWeight) Less(i, j int) bool { return s[i].Weight > s[j].Weight }
|
66
internal/store/testsuite/layer_repository.go
Normal file
66
internal/store/testsuite/layer_repository.go
Normal file
@ -0,0 +1,66 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type layerRepositoryTestCase struct {
|
||||
Name string
|
||||
Do func(repo store.LayerRepository) error
|
||||
}
|
||||
|
||||
const layerType store.LayerType = "test"
|
||||
|
||||
var layerRepositoryTestCases = []layerRepositoryTestCase{
|
||||
{
|
||||
Name: "Create layer",
|
||||
Do: func(repo store.LayerRepository) error {
|
||||
ctx := context.Background()
|
||||
|
||||
options := map[string]any{}
|
||||
|
||||
layer, err := repo.CreateLayer(ctx, "create_layer_proxy", "create_layer", layerType, options)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if layer == nil {
|
||||
return errors.Errorf("layer should not be nil")
|
||||
}
|
||||
|
||||
if layer.Name == "" {
|
||||
return errors.Errorf("layer.Name should not be empty")
|
||||
}
|
||||
|
||||
if layer.Proxy == "" {
|
||||
return errors.Errorf("layer.Proxy should not be empty")
|
||||
}
|
||||
|
||||
if layer.CreatedAt.IsZero() {
|
||||
return errors.Errorf("layer.CreatedAt should not be zero value")
|
||||
}
|
||||
|
||||
if layer.UpdatedAt.IsZero() {
|
||||
return errors.Errorf("layer.UpdatedAt should not be zero value")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestLayerRepository(t *testing.T, repo store.LayerRepository) {
|
||||
for _, tc := range layerRepositoryTestCases {
|
||||
func(tc layerRepositoryTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
if err := tc.Do(repo); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
204
internal/store/testsuite/proxy_repository.go
Normal file
204
internal/store/testsuite/proxy_repository.go
Normal file
@ -0,0 +1,204 @@
|
||||
package testsuite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type proxyRepositoryTestCase struct {
|
||||
Name string
|
||||
Do func(repo store.ProxyRepository) error
|
||||
}
|
||||
|
||||
var proxyRepositoryTestCases = []proxyRepositoryTestCase{
|
||||
{
|
||||
Name: "Create proxy",
|
||||
Do: func(repo store.ProxyRepository) error {
|
||||
ctx := context.Background()
|
||||
|
||||
to := "http://example.com"
|
||||
|
||||
proxy, err := repo.CreateProxy(ctx, "create_proxy", to, "*:*")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if proxy == nil {
|
||||
return errors.Errorf("proxy should not be nil")
|
||||
}
|
||||
|
||||
if proxy.Name == "" {
|
||||
return errors.Errorf("proxy.Name should not be empty")
|
||||
}
|
||||
|
||||
if proxy.To == "" {
|
||||
return errors.Errorf("proxy.To should not be empty")
|
||||
}
|
||||
|
||||
if e, g := to, proxy.To; e != g {
|
||||
return errors.Errorf("proxy.To: expected '%v', got '%v'", to, proxy.To)
|
||||
}
|
||||
|
||||
if proxy.CreatedAt.IsZero() {
|
||||
return errors.Errorf("proxy.CreatedAt should not be zero value")
|
||||
}
|
||||
|
||||
if proxy.UpdatedAt.IsZero() {
|
||||
return errors.Errorf("proxy.UpdatedAt should not be zero value")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Create then get proxy",
|
||||
Do: func(repo store.ProxyRepository) error {
|
||||
ctx := context.Background()
|
||||
|
||||
to := "http://example.com"
|
||||
|
||||
createdProxy, err := repo.CreateProxy(ctx, "create_then_get_proxy", to, "127.0.0.1:*", "localhost:*")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
foundProxy, err := repo.GetProxy(ctx, createdProxy.Name)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if e, g := createdProxy.Name, foundProxy.Name; e != g {
|
||||
return errors.Errorf("foundProxy.Name: expected '%v', got '%v'", createdProxy.Name, foundProxy.Name)
|
||||
}
|
||||
|
||||
if e, g := createdProxy.From, foundProxy.From; !reflect.DeepEqual(e, g) {
|
||||
return errors.Errorf("foundProxy.From: expected '%v', got '%v'", createdProxy.From, foundProxy.From)
|
||||
}
|
||||
|
||||
if e, g := createdProxy.To, foundProxy.To; e != g {
|
||||
return errors.Errorf("foundProxy.To: expected '%v', got '%v'", createdProxy.To, foundProxy.To)
|
||||
}
|
||||
|
||||
if e, g := createdProxy.CreatedAt, foundProxy.CreatedAt; e != g {
|
||||
return errors.Errorf("foundProxy.CreatedAt: expected '%v', got '%v'", createdProxy.CreatedAt, foundProxy.CreatedAt)
|
||||
}
|
||||
|
||||
if e, g := createdProxy.UpdatedAt, foundProxy.UpdatedAt; e != g {
|
||||
return errors.Errorf("foundProxy.UpdatedAt: expected '%v', got '%v'", createdProxy.UpdatedAt, foundProxy.UpdatedAt)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Create then delete proxy",
|
||||
Do: func(repo store.ProxyRepository) error {
|
||||
ctx := context.Background()
|
||||
|
||||
to := "http://example.com"
|
||||
|
||||
createdProxy, err := repo.CreateProxy(ctx, "create_then_delete_proxy", to, "127.0.0.1:*", "localhost:*")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := repo.DeleteProxy(ctx, createdProxy.Name); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
foundProxy, err := repo.GetProxy(ctx, createdProxy.Name)
|
||||
if err == nil {
|
||||
return errors.New("err should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, store.ErrNotFound) {
|
||||
return errors.Errorf("err should be store.ErrNotFound, got '%+v'", err)
|
||||
}
|
||||
|
||||
if foundProxy != nil {
|
||||
return errors.Errorf("foundProxy should be nil, got '%v'", foundProxy)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Create then query",
|
||||
Do: func(repo store.ProxyRepository) error {
|
||||
ctx := context.Background()
|
||||
|
||||
to := "http://example.com"
|
||||
|
||||
createdProxy, err := repo.CreateProxy(ctx, "create_then_query", to, "127.0.0.1:*", "localhost:*")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
headers, err := repo.QueryProxy(ctx)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if len(headers) < 1 {
|
||||
return errors.Errorf("len(headers): expected value > 1, got '%v'", len(headers))
|
||||
}
|
||||
|
||||
found := false
|
||||
|
||||
for _, h := range headers {
|
||||
if h.Name == createdProxy.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return errors.New("could not find created proxy in query results")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Create already existing proxy",
|
||||
Do: func(repo store.ProxyRepository) error {
|
||||
ctx := context.Background()
|
||||
|
||||
to := "http://example.com"
|
||||
|
||||
var name store.ProxyName = "create_already_existing_proxy"
|
||||
|
||||
_, err := repo.CreateProxy(ctx, name, to, "127.0.0.1:*", "localhost:*")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err = repo.CreateProxy(ctx, name, to, "127.0.0.1:*")
|
||||
if err == nil {
|
||||
return errors.New("err should not be nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, store.ErrAlreadyExist) {
|
||||
return errors.Errorf("err: expected store.ErrAlreadyExists, got '%+v'", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestProxyRepository(t *testing.T, repo store.ProxyRepository) {
|
||||
for _, tc := range proxyRepositoryTestCases {
|
||||
func(tc proxyRepositoryTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
if err := tc.Do(repo); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
})
|
||||
}(tc)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user