From 3412d6c75dc984db82f8be5b7acc9c3c2129aa48 Mon Sep 17 00:00:00 2001 From: Valentin Yordanov <valentin.yordanov@vereign.com> Date: Mon, 11 Dec 2023 10:37:14 +0000 Subject: [PATCH] Get cache data with multiple scopes --- .gitlab-ci.yml | 4 +- deployment/ci/Dockerfile | 2 +- deployment/compose/Dockerfile | 2 +- design/design.go | 8 +- design/types.go | 3 +- gen/cache/service.go | 1 + gen/http/cache/client/cli.go | 13 +- gen/http/cache/client/encode_decode.go | 4 + gen/http/cache/server/encode_decode.go | 7 +- gen/http/cache/server/types.go | 3 +- gen/http/cli/cache/cli.go | 14 +- gen/http/openapi.json | 2 +- gen/http/openapi.yaml | 29 ++-- gen/http/openapi3.json | 2 +- gen/http/openapi3.yaml | 62 +++++--- internal/service/cache/service.go | 135 +++++++++++++++-- internal/service/cache/service_test.go | 138 +++++++++++++++++- .../zap/zaptest/observer/logged_entry.go | Bin 0 -> 1596 bytes .../zap/zaptest/observer/observer.go | Bin 0 -> 5725 bytes vendor/modules.txt | Bin 10911 -> 10944 bytes 20 files changed, 362 insertions(+), 67 deletions(-) create mode 100644 vendor/go.uber.org/zap/zaptest/observer/logged_entry.go create mode 100644 vendor/go.uber.org/zap/zaptest/observer/observer.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7ba775a..c92e869 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,7 +14,7 @@ linters: - golangci-lint run unit tests: - image: golang:1.21.3 + image: golang:1.21.5 stage: test script: - go version @@ -23,7 +23,7 @@ unit tests: coverage: '/total:\s+\(statements\)\s+(\d+.\d+\%)/' govulncheck: - image: golang:1.21.3 + image: golang:1.21.5 stage: test script: - go version diff --git a/deployment/ci/Dockerfile b/deployment/ci/Dockerfile index 60f9b82..346b141 100644 --- a/deployment/ci/Dockerfile +++ b/deployment/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.3-alpine3.17 as builder +FROM golang:1.21.5-alpine3.17 as builder RUN apk add git diff --git a/deployment/compose/Dockerfile b/deployment/compose/Dockerfile index e9d3bae..b57e8bd 100644 --- a/deployment/compose/Dockerfile +++ b/deployment/compose/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.3 +FROM golang:1.21.5 RUN go install github.com/ysmood/kit/cmd/guard@v0.25.11 diff --git a/design/design.go b/design/design.go index 470b6a6..28fc20a 100644 --- a/design/design.go +++ b/design/design.go @@ -56,7 +56,13 @@ var _ = Service("cache", func() { Example("Login") }) Header("scope:x-cache-scope", String, "Cache entry scope", func() { - Example("administration") + Example("multiple scopes", "administration,user") + Example("default", "administration") + }) + Header("strategy:x-cache-flatten-strategy", String, "Flatten strategy.", func() { + Example("first key value only", "first") + Example("last key value only", "last") + Example("default", "merge") }) Response(StatusOK, func() { diff --git a/design/types.go b/design/types.go index eee0825..bf8bd71 100644 --- a/design/types.go +++ b/design/types.go @@ -6,7 +6,8 @@ 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 + Field(3, "scope", String) + Field(4, "strategy", String) Required("key") }) diff --git a/gen/cache/service.go b/gen/cache/service.go index 74f41d9..9d0f802 100644 --- a/gen/cache/service.go +++ b/gen/cache/service.go @@ -36,6 +36,7 @@ type CacheGetRequest struct { Key string Namespace *string Scope *string + Strategy *string } // CacheSetRequest is the payload type of the cache service Set method. diff --git a/gen/http/cache/client/cli.go b/gen/http/cache/client/cli.go index 4f75bd4..31684f6 100644 --- a/gen/http/cache/client/cli.go +++ b/gen/http/cache/client/cli.go @@ -16,7 +16,7 @@ import ( ) // BuildGetPayload builds the payload for the cache Get endpoint from CLI flags. -func BuildGetPayload(cacheGetKey string, cacheGetNamespace string, cacheGetScope string) (*cache.CacheGetRequest, error) { +func BuildGetPayload(cacheGetKey string, cacheGetNamespace string, cacheGetScope string, cacheGetStrategy string) (*cache.CacheGetRequest, error) { var key string { key = cacheGetKey @@ -33,10 +33,17 @@ func BuildGetPayload(cacheGetKey string, cacheGetNamespace string, cacheGetScope scope = &cacheGetScope } } + var strategy *string + { + if cacheGetStrategy != "" { + strategy = &cacheGetStrategy + } + } v := &cache.CacheGetRequest{} v.Key = key v.Namespace = namespace v.Scope = scope + v.Strategy = strategy return v, nil } @@ -48,7 +55,7 @@ func BuildSetPayload(cacheSetBody string, cacheSetKey string, cacheSetNamespace { err = json.Unmarshal([]byte(cacheSetBody), &body) if err != nil { - return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Dolorem et quam et illum.\"") + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Enim vel.\"") } } var key string @@ -99,7 +106,7 @@ func BuildSetExternalPayload(cacheSetExternalBody string, cacheSetExternalKey st { err = json.Unmarshal([]byte(cacheSetExternalBody), &body) if err != nil { - return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Quis rerum velit sunt rerum dignissimos at.\"") + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "\"Sint ipsa fugiat et id rem.\"") } } var key string diff --git a/gen/http/cache/client/encode_decode.go b/gen/http/cache/client/encode_decode.go index db80d15..793256a 100644 --- a/gen/http/cache/client/encode_decode.go +++ b/gen/http/cache/client/encode_decode.go @@ -54,6 +54,10 @@ func EncodeGetRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Re head := *p.Scope req.Header.Set("x-cache-scope", head) } + if p.Strategy != nil { + head := *p.Strategy + req.Header.Set("x-cache-flatten-strategy", head) + } return nil } } diff --git a/gen/http/cache/server/encode_decode.go b/gen/http/cache/server/encode_decode.go index 44aee8d..f8d56c0 100644 --- a/gen/http/cache/server/encode_decode.go +++ b/gen/http/cache/server/encode_decode.go @@ -38,6 +38,7 @@ func DecodeGetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Dec key string namespace *string scope *string + strategy *string err error ) key = r.Header.Get("x-cache-key") @@ -52,10 +53,14 @@ func DecodeGetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Dec if scopeRaw != "" { scope = &scopeRaw } + strategyRaw := r.Header.Get("x-cache-flatten-strategy") + if strategyRaw != "" { + strategy = &strategyRaw + } if err != nil { return nil, err } - payload := NewGetCacheGetRequest(key, namespace, scope) + payload := NewGetCacheGetRequest(key, namespace, scope, strategy) return payload, nil } diff --git a/gen/http/cache/server/types.go b/gen/http/cache/server/types.go index 56803f1..1e45878 100644 --- a/gen/http/cache/server/types.go +++ b/gen/http/cache/server/types.go @@ -12,11 +12,12 @@ import ( ) // NewGetCacheGetRequest builds a cache service Get endpoint payload. -func NewGetCacheGetRequest(key string, namespace *string, scope *string) *cache.CacheGetRequest { +func NewGetCacheGetRequest(key string, namespace *string, scope *string, strategy *string) *cache.CacheGetRequest { v := &cache.CacheGetRequest{} v.Key = key v.Namespace = namespace v.Scope = scope + v.Strategy = strategy return v } diff --git a/gen/http/cli/cache/cli.go b/gen/http/cli/cache/cli.go index 44fb882..518b211 100644 --- a/gen/http/cli/cache/cli.go +++ b/gen/http/cli/cache/cli.go @@ -30,7 +30,7 @@ health (liveness|readiness) // UsageExamples produces an example of a valid invocation of the CLI tool. func UsageExamples() string { - return os.Args[0] + ` cache get --key "Voluptatum modi tenetur tempore quia est ratione." --namespace "Corrupti sunt dolores." --scope "Repellat omnis id ex."` + "\n" + + return os.Args[0] + ` cache get --key "Iusto consequatur voluptatem eligendi et eligendi." --namespace "Optio natus." --scope "Ratione quasi perspiciatis qui." --strategy "Animi non alias occaecati esse."` + "\n" + os.Args[0] + ` health liveness` + "\n" + "" } @@ -51,6 +51,7 @@ func ParseEndpoint( cacheGetKeyFlag = cacheGetFlags.String("key", "REQUIRED", "") cacheGetNamespaceFlag = cacheGetFlags.String("namespace", "", "") cacheGetScopeFlag = cacheGetFlags.String("scope", "", "") + cacheGetStrategyFlag = cacheGetFlags.String("strategy", "", "") cacheSetFlags = flag.NewFlagSet("set", flag.ExitOnError) cacheSetBodyFlag = cacheSetFlags.String("body", "REQUIRED", "") @@ -163,7 +164,7 @@ func ParseEndpoint( switch epn { case "get": endpoint = c.Get() - data, err = cachec.BuildGetPayload(*cacheGetKeyFlag, *cacheGetNamespaceFlag, *cacheGetScopeFlag) + data, err = cachec.BuildGetPayload(*cacheGetKeyFlag, *cacheGetNamespaceFlag, *cacheGetScopeFlag, *cacheGetStrategyFlag) case "set": endpoint = c.Set() data, err = cachec.BuildSetPayload(*cacheSetBodyFlag, *cacheSetKeyFlag, *cacheSetNamespaceFlag, *cacheSetScopeFlag, *cacheSetTTLFlag) @@ -206,15 +207,16 @@ Additional help: `, os.Args[0]) } func cacheGetUsage() { - fmt.Fprintf(os.Stderr, `%[1]s [flags] cache get -key STRING -namespace STRING -scope STRING + fmt.Fprintf(os.Stderr, `%[1]s [flags] cache get -key STRING -namespace STRING -scope STRING -strategy STRING Get JSON value from the cache. -key STRING: -namespace STRING: -scope STRING: + -strategy STRING: Example: - %[1]s cache get --key "Voluptatum modi tenetur tempore quia est ratione." --namespace "Corrupti sunt dolores." --scope "Repellat omnis id ex." + %[1]s cache get --key "Iusto consequatur voluptatem eligendi et eligendi." --namespace "Optio natus." --scope "Ratione quasi perspiciatis qui." --strategy "Animi non alias occaecati esse." `, os.Args[0]) } @@ -229,7 +231,7 @@ Set a JSON value in the cache. -ttl INT: Example: - %[1]s cache set --body "Dolorem et quam et illum." --key "Esse inventore ullam placeat aut." --namespace "Omnis itaque." --scope "Quasi ut." --ttl 3100652887298323449 + %[1]s cache set --body "Enim vel." --key "Ut in." --namespace "Ab dolores distinctio quis." --scope "Optio aliquam error nam." --ttl 2227603043401673122 `, os.Args[0]) } @@ -244,7 +246,7 @@ Set an external JSON value in the cache and provide an event for the input. -ttl INT: Example: - %[1]s cache set-external --body "Quis rerum velit sunt rerum dignissimos at." --key "Optio aliquam error nam." --namespace "Recusandae illo." --scope "Placeat veniam veritatis doloribus." --ttl 5137048679846705449 + %[1]s cache set-external --body "Sint ipsa fugiat et id rem." --key "Molestiae minima." --namespace "Quia dolores rem." --scope "Est illum." --ttl 6207033275224297400 `, os.Args[0]) } diff --git a/gen/http/openapi.json b/gen/http/openapi.json index febee9e..81d8d61 100644 --- a/gen/http/openapi.json +++ b/gen/http/openapi.json @@ -1 +1 @@ -{"swagger":"2.0","info":{"title":"Cache Service","description":"The cache service exposes interface for working with Redis.","version":""},"host":"localhost:8083","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/HealthLivenessResponseBody","required":["service","status","version"]}}},"schemes":["http"]}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/HealthReadinessResponseBody","required":["service","status","version"]}}},"schemes":["http"]}},"/v1/cache":{"get":{"tags":["cache"],"summary":"Get cache","description":"Get JSON value from the cache.","operationId":"cache#Get","produces":["application/json"],"parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":false,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":false,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"binary"}}},"schemes":["http"]},"post":{"tags":["cache"],"summary":"Set cache","description":"Set a JSON value in the cache.","operationId":"cache#Set","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":false,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":false,"type":"string"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","required":false,"type":"integer"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"201":{"description":"Created response."}},"schemes":["http"]}},"/v1/external/cache":{"post":{"tags":["cache"],"summary":"SetExternal cache","description":"Set an external JSON value in the cache and provide an event for the input.","operationId":"cache#SetExternal","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":false,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":false,"type":"string"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","required":false,"type":"integer"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"200":{"description":"OK response."}},"schemes":["http"]}}},"definitions":{"HealthLivenessResponseBody":{"title":"HealthLivenessResponseBody","type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Laborum reprehenderit rerum est et ut dolores."},"status":{"type":"string","description":"Status message.","example":"Consequatur porro qui est dolor a."},"version":{"type":"string","description":"Service runtime version.","example":"Ad dolor."}},"example":{"service":"Fugiat voluptatem vel et.","status":"Sint tempore est nam iusto.","version":"Ipsam quidem aut velit vitae est."},"required":["service","status","version"]},"HealthReadinessResponseBody":{"title":"HealthReadinessResponseBody","type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Repellat qui totam et recusandae."},"status":{"type":"string","description":"Status message.","example":"Dolores at qui aliquam ullam."},"version":{"type":"string","description":"Service runtime version.","example":"Omnis ex fugit corporis."}},"example":{"service":"Minima sed et.","status":"Veniam optio qui.","version":"Suscipit velit aliquid et."},"required":["service","status","version"]}}} \ No newline at end of file +{"swagger":"2.0","info":{"title":"Cache Service","description":"The cache service exposes interface for working with Redis.","version":""},"host":"localhost:8083","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/HealthLivenessResponseBody","required":["service","status","version"]}}},"schemes":["http"]}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/HealthReadinessResponseBody","required":["service","status","version"]}}},"schemes":["http"]}},"/v1/cache":{"get":{"tags":["cache"],"summary":"Get cache","description":"Get JSON value from the cache.","operationId":"cache#Get","produces":["application/json"],"parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":false,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":false,"type":"string"},{"name":"x-cache-flatten-strategy","in":"header","description":"Flatten strategy.","required":false,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"binary"}}},"schemes":["http"]},"post":{"tags":["cache"],"summary":"Set cache","description":"Set a JSON value in the cache.","operationId":"cache#Set","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":false,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":false,"type":"string"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","required":false,"type":"integer"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"201":{"description":"Created response."}},"schemes":["http"]}},"/v1/external/cache":{"post":{"tags":["cache"],"summary":"SetExternal cache","description":"Set an external JSON value in the cache and provide an event for the input.","operationId":"cache#SetExternal","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","required":true,"type":"string"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","required":false,"type":"string"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","required":false,"type":"string"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","required":false,"type":"integer"},{"name":"any","in":"body","required":true,"schema":{"type":"string","format":"binary"}}],"responses":{"200":{"description":"OK response."}},"schemes":["http"]}}},"definitions":{"HealthLivenessResponseBody":{"title":"HealthLivenessResponseBody","type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Repellat qui totam et recusandae."},"status":{"type":"string","description":"Status message.","example":"Dolores at qui aliquam ullam."},"version":{"type":"string","description":"Service runtime version.","example":"Omnis ex fugit corporis."}},"example":{"service":"Minima sed et.","status":"Veniam optio qui.","version":"Suscipit velit aliquid et."},"required":["service","status","version"]},"HealthReadinessResponseBody":{"title":"HealthReadinessResponseBody","type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Rerum et qui alias qui."},"status":{"type":"string","description":"Status message.","example":"Beatae commodi."},"version":{"type":"string","description":"Service runtime version.","example":"Fuga voluptas explicabo et libero."}},"example":{"service":"Illum nam ratione nisi.","status":"Labore vel.","version":"Aut illum."},"required":["service","status","version"]}}} \ No newline at end of file diff --git a/gen/http/openapi.yaml b/gen/http/openapi.yaml index 3648e0c..0e5f69c 100644 --- a/gen/http/openapi.yaml +++ b/gen/http/openapi.yaml @@ -72,6 +72,11 @@ paths: description: Cache entry scope required: false type: string + - name: x-cache-flatten-strategy + in: header + description: Flatten strategy. + required: false + type: string responses: "200": description: OK response. @@ -165,19 +170,19 @@ definitions: service: type: string description: Service name. - example: Laborum reprehenderit rerum est et ut dolores. + example: Repellat qui totam et recusandae. status: type: string description: Status message. - example: Consequatur porro qui est dolor a. + example: Dolores at qui aliquam ullam. version: type: string description: Service runtime version. - example: Ad dolor. + example: Omnis ex fugit corporis. example: - service: Fugiat voluptatem vel et. - status: Sint tempore est nam iusto. - version: Ipsam quidem aut velit vitae est. + service: Minima sed et. + status: Veniam optio qui. + version: Suscipit velit aliquid et. required: - service - status @@ -189,19 +194,19 @@ definitions: service: type: string description: Service name. - example: Repellat qui totam et recusandae. + example: Rerum et qui alias qui. status: type: string description: Status message. - example: Dolores at qui aliquam ullam. + example: Beatae commodi. version: type: string description: Service runtime version. - example: Omnis ex fugit corporis. + example: Fuga voluptas explicabo et libero. example: - service: Minima sed et. - status: Veniam optio qui. - version: Suscipit velit aliquid et. + service: Illum nam ratione nisi. + status: Labore vel. + version: Aut illum. required: - service - status diff --git a/gen/http/openapi3.json b/gen/http/openapi3.json index 605856b..c1681f1 100644 --- a/gen/http/openapi3.json +++ b/gen/http/openapi3.json @@ -1 +1 @@ -{"openapi":"3.0.3","info":{"title":"Cache Service","description":"The cache service exposes interface for working with Redis.","version":"1.0"},"servers":[{"url":"http://localhost:8083","description":"Cache Server"}],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"service":"Molestiae minima.","status":"Quia dolores rem.","version":"Est illum."}}}}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"service":"Repellendus quo.","status":"Cumque aut.","version":"Sit sint ipsa fugiat et id rem."}}}}}}},"/v1/cache":{"get":{"tags":["cache"],"summary":"Get cache","description":"Get JSON value from the cache.","operationId":"cache#Get","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Rerum et qui alias qui.","format":"binary"},"example":"Accusamus ratione voluptatibus."}}}}},"post":{"tags":["cache"],"summary":"Set cache","description":"Set a JSON value in the cache.","operationId":"cache#Set","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","allowEmptyValue":true,"schema":{"type":"integer","description":"Cache entry TTL in seconds","example":60,"format":"int64"},"example":60}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Beatae commodi.","format":"binary"},"example":"Quae minus maiores nulla deleniti ipsa."}}},"responses":{"201":{"description":"Created response."}}}},"/v1/external/cache":{"post":{"tags":["cache"],"summary":"SetExternal cache","description":"Set an external JSON value in the cache and provide an event for the input.","operationId":"cache#SetExternal","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","allowEmptyValue":true,"schema":{"type":"integer","description":"Cache entry TTL in seconds","example":60,"format":"int64"},"example":60}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Fuga voluptas explicabo et libero.","format":"binary"},"example":"Quidem nihil quis tempore."}}},"responses":{"200":{"description":"OK response."}}}}},"components":{"schemas":{"HealthResponse":{"type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Illum nam ratione nisi."},"status":{"type":"string","description":"Status message.","example":"Labore vel."},"version":{"type":"string","description":"Service runtime version.","example":"Aut illum."}},"example":{"service":"Itaque vel.","status":"Itaque enim aut consequatur beatae ut.","version":"Et atque impedit nostrum perspiciatis ipsum."},"required":["service","status","version"]}}},"tags":[{"name":"cache","description":"Cache service allows storing and retrieving data from distributed cache."},{"name":"health","description":"Health service provides health check endpoints."}]} \ No newline at end of file +{"openapi":"3.0.3","info":{"title":"Cache Service","description":"The cache service exposes interface for working with Redis.","version":"1.0"},"servers":[{"url":"http://localhost:8083","description":"Cache Server"}],"paths":{"/liveness":{"get":{"tags":["health"],"summary":"Liveness health","operationId":"health#Liveness","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"service":"Laborum reprehenderit rerum est et ut dolores.","status":"Consequatur porro qui est dolor a.","version":"Ad dolor."}}}}}}},"/readiness":{"get":{"tags":["health"],"summary":"Readiness health","operationId":"health#Readiness","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"},"example":{"service":"Fugiat voluptatem vel et.","status":"Sint tempore est nam iusto.","version":"Ipsam quidem aut velit vitae est."}}}}}}},"/v1/cache":{"get":{"tags":["cache"],"summary":"Get cache","description":"Get JSON value from the cache.","operationId":"cache#Get","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"examples":{"default":{"summary":"default","value":"administration"},"multiple scopes":{"summary":"multiple scopes","value":"administration,user"}}},{"name":"x-cache-flatten-strategy","in":"header","description":"Flatten strategy.","allowEmptyValue":true,"schema":{"type":"string","description":"Flatten strategy.","example":"merge"},"examples":{"default":{"summary":"default","value":"merge"},"first key value only":{"summary":"first key value only","value":"first"},"last key value only":{"summary":"last key value only","value":"last"}}}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"type":"string","example":"Itaque vel.","format":"binary"},"example":"Amet atque cupiditate."}}}}},"post":{"tags":["cache"],"summary":"Set cache","description":"Set a JSON value in the cache.","operationId":"cache#Set","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","allowEmptyValue":true,"schema":{"type":"integer","description":"Cache entry TTL in seconds","example":60,"format":"int64"},"example":60}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Itaque enim aut consequatur beatae ut.","format":"binary"},"example":"Minus nostrum eaque."}}},"responses":{"201":{"description":"Created response."}}}},"/v1/external/cache":{"post":{"tags":["cache"],"summary":"SetExternal cache","description":"Set an external JSON value in the cache and provide an event for the input.","operationId":"cache#SetExternal","parameters":[{"name":"x-cache-key","in":"header","description":"Cache entry key","allowEmptyValue":true,"required":true,"schema":{"type":"string","description":"Cache entry key","example":"did:web:example.com"},"example":"did:web:example.com"},{"name":"x-cache-namespace","in":"header","description":"Cache entry namespace","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry namespace","example":"Login"},"example":"Login"},{"name":"x-cache-scope","in":"header","description":"Cache entry scope","allowEmptyValue":true,"schema":{"type":"string","description":"Cache entry scope","example":"administration"},"example":"administration"},{"name":"x-cache-ttl","in":"header","description":"Cache entry TTL in seconds","allowEmptyValue":true,"schema":{"type":"integer","description":"Cache entry TTL in seconds","example":60,"format":"int64"},"example":60}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"string","example":"Et atque impedit nostrum perspiciatis ipsum.","format":"binary"},"example":"Eligendi magni qui porro beatae porro."}}},"responses":{"200":{"description":"OK response."}}}}},"components":{"schemas":{"HealthResponse":{"type":"object","properties":{"service":{"type":"string","description":"Service name.","example":"Accusamus ratione voluptatibus."},"status":{"type":"string","description":"Status message.","example":"Quae minus maiores nulla deleniti ipsa."},"version":{"type":"string","description":"Service runtime version.","example":"Quidem nihil quis tempore."}},"example":{"service":"Vel quis doloremque iure eius reiciendis.","status":"Perferendis porro laborum autem dolorem aut nesciunt.","version":"Sed eius qui placeat sed."},"required":["service","status","version"]}}},"tags":[{"name":"cache","description":"Cache service allows storing and retrieving data from distributed cache."},{"name":"health","description":"Health service provides health check endpoints."}]} \ No newline at end of file diff --git a/gen/http/openapi3.yaml b/gen/http/openapi3.yaml index 61ccd2d..0c90f30 100644 --- a/gen/http/openapi3.yaml +++ b/gen/http/openapi3.yaml @@ -21,9 +21,9 @@ paths: schema: $ref: '#/components/schemas/HealthResponse' example: - service: Molestiae minima. - status: Quia dolores rem. - version: Est illum. + service: Laborum reprehenderit rerum est et ut dolores. + status: Consequatur porro qui est dolor a. + version: Ad dolor. /readiness: get: tags: @@ -38,9 +38,9 @@ paths: schema: $ref: '#/components/schemas/HealthResponse' example: - service: Repellendus quo. - status: Cumque aut. - version: Sit sint ipsa fugiat et id rem. + service: Fugiat voluptatem vel et. + status: Sint tempore est nam iusto. + version: Ipsam quidem aut velit vitae est. /v1/cache: get: tags: @@ -76,7 +76,31 @@ paths: type: string description: Cache entry scope example: administration - example: administration + examples: + default: + summary: default + value: administration + multiple scopes: + summary: multiple scopes + value: administration,user + - name: x-cache-flatten-strategy + in: header + description: Flatten strategy. + allowEmptyValue: true + schema: + type: string + description: Flatten strategy. + example: merge + examples: + default: + summary: default + value: merge + first key value only: + summary: first key value only + value: first + last key value only: + summary: last key value only + value: last responses: "200": description: OK response. @@ -84,9 +108,9 @@ paths: application/json: schema: type: string - example: Rerum et qui alias qui. + example: Itaque vel. format: binary - example: Accusamus ratione voluptatibus. + example: Amet atque cupiditate. post: tags: - cache @@ -138,9 +162,9 @@ paths: application/json: schema: type: string - example: Beatae commodi. + example: Itaque enim aut consequatur beatae ut. format: binary - example: Quae minus maiores nulla deleniti ipsa. + example: Minus nostrum eaque. responses: "201": description: Created response. @@ -196,9 +220,9 @@ paths: application/json: schema: type: string - example: Fuga voluptas explicabo et libero. + example: Et atque impedit nostrum perspiciatis ipsum. format: binary - example: Quidem nihil quis tempore. + example: Eligendi magni qui porro beatae porro. responses: "200": description: OK response. @@ -210,19 +234,19 @@ components: service: type: string description: Service name. - example: Illum nam ratione nisi. + example: Accusamus ratione voluptatibus. status: type: string description: Status message. - example: Labore vel. + example: Quae minus maiores nulla deleniti ipsa. version: type: string description: Service runtime version. - example: Aut illum. + example: Quidem nihil quis tempore. example: - service: Itaque vel. - status: Itaque enim aut consequatur beatae ut. - version: Et atque impedit nostrum perspiciatis ipsum. + service: Vel quis doloremque iure eius reiciendis. + status: Perferendis porro laborum autem dolorem aut nesciunt. + version: Sed eius qui placeat sed. required: - service - status diff --git a/internal/service/cache/service.go b/internal/service/cache/service.go index 9fdb01c..22de0e1 100644 --- a/internal/service/cache/service.go +++ b/internal/service/cache/service.go @@ -3,6 +3,8 @@ package cache import ( "context" "encoding/json" + "fmt" + "strings" "time" "go.uber.org/zap" @@ -45,21 +47,19 @@ func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) (interfac return nil, errors.New(errors.BadRequest, "missing key") } - // create key from the input fields - key := makeCacheKey(req.Key, req.Namespace, req.Scope) - data, err := s.cache.Get(ctx, key) - if err != nil { - 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) + var scopes []string + if req.Scope != nil { + scopes = strings.Split(*req.Scope, ",") } - var decodedValue interface{} - if err := json.Unmarshal(data, &decodedValue); err != nil { - logger.Error("cannot decode json value from cache", zap.Error(err)) - return nil, errors.New("cannot decode json value from cache", err) + if len(scopes) > 1 { + return s.getWithMultipleScopes(ctx, req, scopes) + } + + decodedValue, err := s.get(ctx, req.Key, req.Namespace, req.Scope) + if err != nil { + logger.Error("error getting value from cache", zap.Error(err)) + return nil, err } return decodedValue, nil @@ -128,3 +128,112 @@ func makeCacheKey(key string, namespace, scope *string) string { } return k } + +func (s *Service) getWithMultipleScopes(ctx context.Context, req *cache.CacheGetRequest, scopes []string) (map[string]interface{}, error) { + keyValues := map[string][]interface{}{} + result := map[string]interface{}{} + + for _, scope := range scopes { + scope := strings.TrimSpace(scope) + decodedValue, err := s.get(ctx, req.Key, req.Namespace, &scope) + if err != nil { + if errors.Is(errors.NotFound, err) { + s.logger.Warn(err.Error()) + continue + } + return nil, err + } + + switch d := decodedValue.(type) { + case map[string]interface{}: + addValue(d, keyValues) + case []map[string]interface{}: + for _, data := range d { + addValue(data, keyValues) + } + default: + s.logger.Warn("decode value is of unknown type") + continue + } + } + + switch { + case req.Strategy == nil || *req.Strategy == "merge": + result = mergeAll(keyValues) + + case *req.Strategy == "first": + for key, value := range keyValues { + result[key] = value[0] + } + + case *req.Strategy == "last": + for key, value := range keyValues { + result[key] = value[len(value)-1] + } + } + + return result, nil +} + +func addValue(decodedValue map[string]interface{}, des map[string][]interface{}) { + for k, v := range decodedValue { + if dataArr, contains := des[k]; contains { + dataArr = append(dataArr, v) + des[k] = dataArr + continue + } + + des[k] = []interface{}{v} + } +} + +// mergeAll merges all values for a key if more than one value is available. +func mergeAll(data map[string][]interface{}) map[string]interface{} { + result := map[string]interface{}{} + for key, value := range data { + if len(value) == 1 { + result[key] = value[0] + continue + } + + for i, v := range value { + index := fmt.Sprintf("%s_%d", key, i+1) + result[index] = v + } + } + return result +} + +func (s *Service) get(ctx context.Context, key string, namespace *string, scope *string) (interface{}, error) { + // create key from the input fields + cacheKey := makeCacheKey(key, namespace, scope) + data, err := s.cache.Get(ctx, cacheKey) + if err != nil { + if errors.Is(errors.NotFound, err) { + return nil, errors.New(errors.NotFound, "key not found in cache", err) + } + return nil, errors.New("error getting value from cache", err) + } + + decodedValue, err := unmarshalCacheData(data) + if err != nil { + return nil, errors.New("cannot decode json value from cache", err) + } + + return decodedValue, nil +} + +func unmarshalCacheData(data []byte) (interface{}, error) { + var keyValueArray []map[string]interface{} + var keyValue map[string]interface{} + + err := json.Unmarshal(data, &keyValue) + if err != nil { + err := json.Unmarshal(data, &keyValueArray) + if err != nil { + return nil, err + } + return keyValueArray, nil + } + return keyValue, nil +} diff --git a/internal/service/cache/service_test.go b/internal/service/cache/service_test.go index 530d9bf..1d87d4d 100644 --- a/internal/service/cache/service_test.go +++ b/internal/service/cache/service_test.go @@ -2,11 +2,13 @@ package cache_test import ( "context" + "fmt" "testing" "time" "github.com/stretchr/testify/assert" "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" goacache "gitlab.eclipse.org/eclipse/xfsc/tsa/cache/gen/cache" "gitlab.eclipse.org/eclipse/xfsc/tsa/cache/internal/service/cache" @@ -21,14 +23,18 @@ func TestNew(t *testing.T) { } func TestService_Get(t *testing.T) { + const key1 = "key,namespace,scope" + const key2 = "key,namespace,scope2" + tests := []struct { name string cache *cachefakes.FakeCache req *goacache.CacheGetRequest - res interface{} - errkind errors.Kind - errtext string + res interface{} + errkind errors.Kind + errtext string + loggerText string }{ { name: "missing cache key", @@ -96,12 +102,136 @@ func TestService_Get(t *testing.T) { res: map[string]interface{}{"test": "value"}, errtext: "", }, + { + name: "multiple scope cache return error", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("last"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return nil, fmt.Errorf("some error") + } + return []byte(`{"test":"value2"}`), nil + }, + }, + errtext: "error getting value from cache", + }, + { + name: "multiple scope with merge flatten strategy", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("merge"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + return []byte(`{"test":"value2"}`), nil + }, + }, + res: map[string]interface{}{"test_1": "value", "test_2": "value2"}, + errtext: "", + }, + { + name: "multiple scope with first flatten strategy", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("first"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + return []byte(`{"test":"value2"}`), nil + }, + }, + res: map[string]interface{}{"test": "value"}, + errtext: "", + }, + { + name: "multiple scope with last flatten strategy", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,not_existed_scope,scope2"), + Strategy: ptr.String("last"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + return []byte(`{"test":"value2"}`), nil + }, + }, + res: map[string]interface{}{"test": "value2"}, + errtext: "", + }, + { + name: "multiple scope with last flatten strategy", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("last"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + return []byte(`{"test":"value2"}`), nil + }, + }, + res: map[string]interface{}{"test": "value2"}, + errtext: "", + }, + { + name: "multiple scope return warn if the key doesn't exist", + req: &goacache.CacheGetRequest{ + Key: "key", + Namespace: ptr.String("namespace"), + Scope: ptr.String("scope,scope2"), + Strategy: ptr.String("last"), + }, + cache: &cachefakes.FakeCache{ + GetStub: func(ctx context.Context, key string) ([]byte, error) { + if key == key1 { + return []byte(`{"test":"value"}`), nil + } + if key == key2 { + return []byte(`{"test":"value2"}`), nil + } + return nil, errors.New(errors.NotFound, "some error") + }, + }, + res: map[string]interface{}{"test": "value2"}, + loggerText: "key not found in cache", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - svc := cache.New(test.cache, nil, zap.NewNop()) + core, logs := observer.New(zap.WarnLevel) + logger := zap.New(core) + + svc := cache.New(test.cache, nil, logger) res, err := svc.Get(context.Background(), test.req) + + if test.loggerText != "" && logs.Len() >= 1 { + assert.Contains(t, logs.All()[0].Message, test.loggerText) + } + if err == nil { assert.Empty(t, test.errtext) assert.Equal(t, test.res, res) diff --git a/vendor/go.uber.org/zap/zaptest/observer/logged_entry.go b/vendor/go.uber.org/zap/zaptest/observer/logged_entry.go new file mode 100644 index 0000000000000000000000000000000000000000..a4ea7ec36c1e4162f9a56e17183d78be218c4147 GIT binary patch literal 1596 zcmdPbS8&cRs4U7%&nQvQNY+#^GB7k(2u(^YQV2;+&dAHp$xqKrE!I)+%uCke(%0wG z*H;KgEy~R-F3!x)Q^+h<$Ve?pO{!E#FG|cSNlnpFNGnQBRme|MNX|$sN>9~MD9Kky z%&Sx=NG&SP&r`@xDoM=D%gjqxNK{D9FQ^2Wm!GCkl95@gP@JDuQl40ps*sqMqL5fz zoS&STSdyBekdmKVnwy$el2`(=GA%PFwOB!;BqLQpDHy6lNfT^ON@`*b$iU1z1&}mM zR-rtzBqP7HM4>3PxTGjE8Dxx(LS|laPH76rB{1on%-l?<B_QqKFewI^SCX$#TAZo_ zcCd~@ZhlH;TBVLcZYtP+1*J(jnZ+483MrW&)00X|QgsxHL4wJtc_3Yhc`5q&MGD2K zIbg>n=NDwA7K1_vWC_fTI$%$i<b%Q~H?ssp73b#_gF>%7BR?0_Kbc@xrIi-tWfo_o zrhpWs<SP{CgYC^qO)de6fJ{%z&&kOz2l+8MKQAQ{<ndxFXe@?gq$(sP<(H*`y#@BH zLSBAJW-=uF!7);Rh-s))aYkZJjzUr@H26|e6f*NbK?D+o`K$=!{o<0uypqhs9EF1X zBCx&aK8B^H5D!;{V1KueaK|851<zoGfFS=cPZw7g1trH|1<zn59ffeu5D)**5QT8Z zpdd%TkVpl8Hw8z(NCj_CKNlSZ*NA{1*Wh3U{~(Y%JbeRvJY8LM6g>T$eL`J4{oECt zLPHe%{6iFcJbgVwTwN4G{1rfULQV5@4F;L#>l)<j;pi9Q=;Z0+84{@jGRw^~#1CY$ zn}3jkqe6gVP>82<sE=ciLO^IxfPb*7f}@{{f}g*ir=MGpr=Pp4ud82(o`R>Jf}g*F zYnZEFh(fT3qmK{BHjr_Sp&=grK_G`JIQs`g26?)BgeZ9U`?$CU1uHnYD)@LhI{CON zfNk@ORB-lj^z_wHaB=i?bO*UHNWnkE!xijFkV=R%6~aASL82gA9sLv>okKkR{XqV4 z_V)`3a&!*SQ3&x53Q_Qf>JIk|cGXdE4Dt*Hg^XK}zc1LQpwRVq1F7=#Q*ie8^K*q5 z1PXbO^AQo`9|Y!y2D^e(gI%QH;_B$*>E|A-;OPhQCn#`X5vs?<RgjpRotU1gke^hX zT2z)=#Ko1F3rZ6TO6mD}rAetpdih1^`c;VqAe@|El&Zu9GQ}}Z!6!dGJvGHOucW9F zRL~^mDWv8l=Yz7WZen^~esM`=vO-a6K~ZWkyl}`*Q%F?E$xm0vO)V}?Oiu+RG6lEH z)SMKB#InT9oW!KeoXiriamo34C8-r93Mr`tsd*`hc_n&WC6xuKD7F@t6qP2IC{%NC zRwWjI+@=S14Hu^~RC9DJOag2PS1s7>P+8x^0)?W~lG38QVueJ7+{6NfwEQ9kP)<n$ zxu{qnGfx4kRF5mIG%r~}1I2nxn5=JNfrci?fM`$&lb0TwnOBlpl$MyBT3rir95`4~ zixjME6=2TsOD*?JEbvbPl?JY$P)jY+(B$F-IUrs~Aq}LoC^0WRRUuUm>I9HMoN0QF fDJdcO8c-88xj1XNI3a$31b%9f9@wYFT(w*PRaybU literal 0 HcmV?d00001 diff --git a/vendor/go.uber.org/zap/zaptest/observer/observer.go b/vendor/go.uber.org/zap/zaptest/observer/observer.go new file mode 100644 index 0000000000000000000000000000000000000000..f77f1308baf29fa3699df5d3cb0eb07763f50d9b GIT binary patch literal 5725 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 Z4(fc>;>`uYm3he;nhJ2QL1Px85de+rY~TO@ literal 0 HcmV?d00001 diff --git a/vendor/modules.txt b/vendor/modules.txt index ffa5dce85532a46fe8dc891429ca72e3eb6cbf3f..489d9cc44ec0130d660153efea671ef669dd4f01 100644 GIT binary patch delta 45 ycmbOqdLVRzudG6PzFuikYLQ-kQM!IrVgU%3q!yRx=O-1X7L}zIZT683<^=$jIuSDf delta 12 TcmX>QIzM!Suk7YL*&tp3ByI%3 -- GitLab