diff --git a/cmd/cache/main.go b/cmd/cache/main.go index 2217d6fb077101a7922ab2d9db3d99fcc8606806..0db3723ab09cb4cf45928bcbffbdd264324875e6 100644 --- a/cmd/cache/main.go +++ b/cmd/cache/main.go @@ -14,12 +14,16 @@ import ( goa "goa.design/goa/v3/pkg" "golang.org/x/sync/errgroup" + goacache "code.vereign.com/gaiax/tsa/cache/gen/cache" goahealth "code.vereign.com/gaiax/tsa/cache/gen/health" + goacachesrv "code.vereign.com/gaiax/tsa/cache/gen/http/cache/server" goahealthsrv "code.vereign.com/gaiax/tsa/cache/gen/http/health/server" goaopenapisrv "code.vereign.com/gaiax/tsa/cache/gen/http/openapi/server" "code.vereign.com/gaiax/tsa/cache/gen/openapi" + "code.vereign.com/gaiax/tsa/cache/internal/clients/redis" "code.vereign.com/gaiax/tsa/cache/internal/config" "code.vereign.com/gaiax/tsa/cache/internal/service" + "code.vereign.com/gaiax/tsa/cache/internal/service/cache" "code.vereign.com/gaiax/tsa/cache/internal/service/health" "code.vereign.com/gaiax/tsa/golib/graceful" ) @@ -40,20 +44,27 @@ func main() { logger.Info("start cache service", zap.String("version", Version), zap.String("goa", goa.Version())) + // create redis client + redis := redis.New(cfg.Redis.Addr, cfg.Redis.User, cfg.Redis.Pass, cfg.Redis.DB, cfg.Redis.TTL) + // create services var ( + cacheSvc goacache.Service healthSvc goahealth.Service ) { + cacheSvc = cache.New(redis, logger) healthSvc = health.New() } // create endpoints var ( + cacheEndpoints *goacache.Endpoints healthEndpoints *goahealth.Endpoints openapiEndpoints *openapi.Endpoints ) { + cacheEndpoints = goacache.NewEndpoints(cacheSvc) healthEndpoints = goahealth.NewEndpoints(healthSvc) openapiEndpoints = openapi.NewEndpoints(nil) } @@ -72,15 +83,18 @@ func main() { mux := goahttp.NewMuxer() var ( + cacheServer *goacachesrv.Server healthServer *goahealthsrv.Server openapiServer *goaopenapisrv.Server ) { + cacheServer = goacachesrv.New(cacheEndpoints, mux, dec, enc, nil, errFormatter) healthServer = goahealthsrv.New(healthEndpoints, mux, dec, enc, nil, errFormatter) openapiServer = goaopenapisrv.New(openapiEndpoints, mux, dec, enc, nil, errFormatter, nil, nil) } // Configure the mux. + goacachesrv.Mount(mux, cacheServer) goahealthsrv.Mount(mux, healthServer) goaopenapisrv.Mount(mux, openapiServer) diff --git a/design/design.go b/design/design.go index 24898ec3e833d5870f75fea6d0649d6a1d241401..0617832a38b93fa17411bdfbb53bd60df6dd43c0 100644 --- a/design/design.go +++ b/design/design.go @@ -10,7 +10,7 @@ var _ = API("cache", func() { Description("Cache Server") Host("development", func() { Description("Local development server") - URI("http://localhost:8080") + URI("http://localhost:8083") }) }) }) @@ -37,6 +37,33 @@ var _ = Service("health", func() { }) }) +var _ = Service("cache", func() { + Description("Cache service allows storing and retrieving data from distributed cache.") + + Method("Get", func() { + Description("Get value from the cache. The result is a sequence of bytes which the client must decode.") + + Payload(CacheGetRequest) + Result(Bytes) + + HTTP(func() { + GET("/v1/cache") + + Header("key:x-cache-key", String, "Cache entry key", func() { + Example("did:web:example.com") + }) + Header("namespace:x-cache-namespace", String, "Cache entry namespace", func() { + Example("Login") + }) + Header("scope:x-cache-scope", String, "Cache entry scope", func() { + Example("administration") + }) + + Response(StatusOK) + }) + }) +}) + var _ = Service("openapi", func() { Description("The openapi service serves the OpenAPI(v3) definition.") Meta("swagger:generate", "false") diff --git a/design/types.go b/design/types.go new file mode 100644 index 0000000000000000000000000000000000000000..a85991116ba7b668e76b6db6f504c53eb193ba41 --- /dev/null +++ b/design/types.go @@ -0,0 +1,10 @@ +package design + +import . "goa.design/goa/v3/dsl" + +var CacheGetRequest = Type("CacheGetRequest", func() { + Field(1, "key", String) + Field(2, "namespace", String) + Field(3, "scope", String) // Initial implementation with a single scope + Required("key", "namespace", "scope") +}) diff --git a/internal/clients/redis/client.go b/internal/clients/redis/client.go new file mode 100644 index 0000000000000000000000000000000000000000..73fe1086db9187c616f6bc57cfd6b4f138bf3ed4 --- /dev/null +++ b/internal/clients/redis/client.go @@ -0,0 +1,42 @@ +package redis + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" + + "code.vereign.com/gaiax/tsa/golib/errors" +) + +type Client struct { + rdb *redis.Client + defaultTTL time.Duration +} + +func New(addr, user, pass string, db int, defaultTTL time.Duration) *Client { + rdb := redis.NewClient(&redis.Options{ + Addr: addr, + Username: user, + Password: pass, + DB: db, + DialTimeout: 10 * time.Second, + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + }) + + return &Client{ + rdb: rdb, + } +} + +func (c *Client) Get(ctx context.Context, key string) ([]byte, error) { + result := c.rdb.Get(ctx, key) + if result.Err() != nil { + if result.Err() == redis.Nil { + return nil, errors.New(errors.NotFound) + } + return nil, result.Err() + } + return []byte(result.Val()), nil +} diff --git a/internal/config/config.go b/internal/config/config.go index bdb25c11fa2d8aa3c2ea8c19adb6d93c15d65799..38c84ee7304dcf412a17405e872be618118f79e2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,15 +3,24 @@ package config import "time" type Config struct { - HTTP httpConfig + HTTP httpConfig + Redis redisConfig LogLevel string `envconfig:"LOG_LEVEL" default:"INFO"` } type httpConfig struct { Host string `envconfig:"HTTP_HOST"` - Port string `envconfig:"HTTP_PORT" default:"8080"` + Port string `envconfig:"HTTP_PORT" default:"8083"` IdleTimeout time.Duration `envconfig:"HTTP_IDLE_TIMEOUT" default:"120s"` ReadTimeout time.Duration `envconfig:"HTTP_READ_TIMEOUT" default:"10s"` WriteTimeout time.Duration `envconfig:"HTTP_WRITE_TIMEOUT" default:"10s"` } + +type redisConfig struct { + Addr string `envconfig:"REDIS_ADDR" required:"true"` + User string `envconfig:"REDIS_USER" required:"true"` + Pass string `envconfig:"REDIS_PASS" required:"true"` + DB int `envconfig:"REDIS_DB" default:"0"` + TTL time.Duration `envconfig:"REDIS_EXPIRATION"` // no default expiration, keys are set to live forever +} diff --git a/internal/service/cache/service.go b/internal/service/cache/service.go new file mode 100644 index 0000000000000000000000000000000000000000..28744bc09cc300d433ebbbcef861fcee7d6b16a4 --- /dev/null +++ b/internal/service/cache/service.go @@ -0,0 +1,52 @@ +package cache + +import ( + "context" + "fmt" + + "go.uber.org/zap" + + "code.vereign.com/gaiax/tsa/cache/gen/cache" + "code.vereign.com/gaiax/tsa/golib/errors" +) + +type Cache interface { + Get(ctx context.Context, key string) ([]byte, error) +} + +type Service struct { + cache Cache + logger *zap.Logger +} + +func New(cache Cache, logger *zap.Logger) *Service { + return &Service{ + cache: cache, + logger: logger.With(zap.String("service", "cache")), + } +} + +func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) ([]byte, error) { + var operation = zap.String("operation", "get") + if req.Key == "" || req.Namespace == "" || req.Scope == "" { + s.logger.Error("bad request: missing key or namespace or scopes", operation) + return nil, errors.New(errors.BadRequest, "bad request") + } + + // create key from the input fields + key := makeCacheKey(req.Key, req.Namespace, req.Scope) + data, err := s.cache.Get(ctx, key) + if err != nil { + s.logger.Error("error getting value from cache", zap.String("key", key), zap.Error(err)) + if errors.Is(errors.NotFound, err) { + return nil, errors.New("key not found in cache", err) + } + return nil, errors.New("error getting value from cache", err) + } + + return data, nil +} + +func makeCacheKey(key, namespace, scope string) string { + return fmt.Sprintf("%s,%s,%s", namespace, scope, key) +}