diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index db38b59..ad5ff7d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,6 +19,7 @@ jobs: fetch-depth: 2 - name: Build Agent run: make build + working-directory: cmd/edge-agent - name: GoReleaser Check uses: goreleaser/goreleaser-action@v4 with: diff --git a/.gitignore b/.gitignore index af77938..b1c74dc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ .DS_Store build/ dist/ -agent.properties -bin +cmd/edge-agent/agent.properties +cmd/edge-agent/bin diff --git a/Makefile b/Makefile deleted file mode 100644 index bebc0d3..0000000 --- a/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -NAME = edge-agent -BUILD_PATH = bin/$(NAME) -# NOTE: CGO needs to be disabled for running images on Alpine -GOENV = GOARCH=amd64 GOOS=linux CGO_ENABLED=0 -GOCMD = go -GOBUILD = $(GOCMD) build -o - -DOCKER_REPOSITORY = warrantdev -DOCKER_IMAGE = edge-agent -DOCKER_TAG = $(VERSION) -VERSION = $(shell cat VERSION) - -.PHONY: clean -clean: - rm -f $(BUILD_PATH) - -.PHONY: dev -dev: clean - $(GOCMD) get - $(GOBUILD) $(BUILD_PATH) main.go - -.PHONY: build -build: clean - $(GOCMD) get - $(GOENV) $(GOBUILD) $(BUILD_PATH) -ldflags="-s -w" main.go - -.PHONY: docker -docker: - docker build --platform linux/amd64 -t $(DOCKER_REPOSITORY)/$(DOCKER_IMAGE):$(DOCKER_TAG) . - docker build --platform linux/amd64 -t $(DOCKER_REPOSITORY)/$(DOCKER_IMAGE) . - -.PHONY: push -push: docker - docker push $(DOCKER_REPOSITORY)/$(DOCKER_IMAGE):$(DOCKER_TAG) - docker push $(DOCKER_REPOSITORY)/$(DOCKER_IMAGE) diff --git a/client.go b/client.go new file mode 100644 index 0000000..ff87629 --- /dev/null +++ b/client.go @@ -0,0 +1,313 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package edge + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "github.com/pkg/errors" + + "github.com/r3labs/sse" + "gopkg.in/cenkalti/backoff.v1" +) + +const ( + DefaultApiEndpoint = "https://api.warrant.dev" + DefaultStreamingEndpoint = "https://stream.warrant.dev/v1" + DefaultPollingFrequency = 10 + + EventTypeSetWarrants = "set_warrants" + EventTypeDeleteWarrants = "del_warrants" + EventTypeResetWarrants = "reset_warrants" + EventTypeShutdown = "shutdown" + + UpdateStrategyPolling = "POLLING" + UpdateStrategyStreaming = "STREAMING" +) + +var ( + ErrInvalidUpdateStrategy = errors.New("invalid update strategy") + ErrInvalidPollingFrequency = errors.New("invalid polling frequency (cannot be < 10s)") + ErrMissingApiKey = errors.New("missing API key") +) + +type ClientConfig struct { + ApiKey string + ApiEndpoint string + UpdateStrategy string + StreamingEndpoint string + PollingFrequency int + Repository IRepository +} + +type Client struct { + config ClientConfig + streamingClient *sse.Client +} + +func NewClient(conf ClientConfig) (*Client, error) { + config := ClientConfig{ + ApiEndpoint: DefaultApiEndpoint, + StreamingEndpoint: DefaultStreamingEndpoint, + UpdateStrategy: UpdateStrategyPolling, + PollingFrequency: DefaultPollingFrequency, + Repository: conf.Repository, + } + + if conf.ApiKey == "" { + return nil, ErrMissingApiKey + } else { + config.ApiKey = conf.ApiKey + } + + if conf.ApiEndpoint != "" { + config.ApiEndpoint = conf.ApiEndpoint + } + + if conf.StreamingEndpoint != "" { + config.StreamingEndpoint = conf.StreamingEndpoint + } + + if conf.UpdateStrategy != "" { + config.UpdateStrategy = conf.UpdateStrategy + } + + if conf.PollingFrequency != 0 { + config.PollingFrequency = conf.PollingFrequency + } else if config.PollingFrequency < 10 { + return nil, ErrInvalidPollingFrequency + } + + if config.UpdateStrategy == UpdateStrategyStreaming { + streamingClient := sse.NewClient(fmt.Sprintf("%s/events", config.StreamingEndpoint)) + streamingClient.Headers["Authorization"] = fmt.Sprintf("ApiKey %s", config.ApiKey) + streamingClient.ReconnectStrategy = backoff.WithMaxTries(backoff.NewExponentialBackOff(), 10) + streamingClient.ReconnectNotify = reconnectNotify + + return &Client{ + config: config, + streamingClient: streamingClient, + }, nil + } else if config.UpdateStrategy == UpdateStrategyPolling || config.UpdateStrategy == "" { + return &Client{ + config: config, + }, nil + } else { + return nil, ErrInvalidUpdateStrategy + } +} + +func (client *Client) Run() error { + err := client.initialize() + if err != nil { + return errors.Wrap(err, "error trying to initialize edge agent") + } + + if client.config.UpdateStrategy == UpdateStrategyStreaming { + err = client.connect() + if err != nil { + return errors.Wrap(err, "error streaming warrant updates") + } + } else if client.config.UpdateStrategy == UpdateStrategyPolling { + err = client.poll() + if err != nil { + return errors.Wrap(err, "error polling warrant updates") + } + } else { + return ErrInvalidUpdateStrategy + } + + return nil +} + +func (client *Client) initialize() error { + client.config.Repository.SetReady(false) + err := client.config.Repository.Clear() + if err != nil { + return errors.Wrap(err, "error clearing cache") + } + + warrants, err := client.getWarrants() + if err != nil { + return errors.Wrap(err, "error getting warrants") + } + + for warrant, count := range warrants { + err := client.config.Repository.Set(warrant, count) + if err != nil { + return errors.Wrapf(err, "error setting warrant %s in cache", warrant) + } + } + + client.config.Repository.SetReady(true) + return nil +} + +func (client *Client) connect() error { + client.streamingClient.OnDisconnect(client.restart) + err := client.streamingClient.Subscribe(client.config.ApiKey, client.processEvent) + if err != nil { + return err + } + + return nil +} + +func (client *Client) poll() error { + for { + time.Sleep(time.Second * time.Duration(client.config.PollingFrequency)) + warrants, err := client.getWarrants() + if err != nil { + return errors.Wrap(err, "error getting warrants") + } + + err = client.config.Repository.Update(warrants) + if err != nil { + return errors.Wrap(err, "error updating warrants") + } + } +} + +func (client *Client) getWarrants() (WarrantSet, error) { + resp, err := client.makeRequest("GET", fmt.Sprintf("%s/expand", ApiVersion), nil) + if err != nil { + return nil, err + } + + respStatus := resp.StatusCode + if respStatus < 200 || respStatus >= 400 { + msg, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "error reading response from server") + } + + return nil, errors.New(fmt.Sprintf("received HTTP %d: %s", respStatus, string(msg))) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "error reading response from server") + } + + var warrants WarrantSet + err = json.Unmarshal(body, &warrants) + if err != nil { + return nil, errors.Wrap(err, "received invalid response from server") + } + + return warrants, nil +} + +func (client *Client) makeRequest(method string, requestUri string, payload interface{}) (*http.Response, error) { + postBody, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + requestBody := bytes.NewBuffer(postBody) + req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", client.config.ApiEndpoint, requestUri), requestBody) + if err != nil { + return nil, errors.Wrap(err, "error creating request object") + } + + req.Header.Add("Authorization", fmt.Sprintf("ApiKey %s", client.config.ApiKey)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, errors.Wrap(err, "error making request to server") + } + + return resp, nil +} + +func (client *Client) processEvent(event *sse.Event) { + var err error + switch string(event.Event) { + case EventTypeSetWarrants: + err = client.processSetWarrants(event) + case EventTypeDeleteWarrants: + err = client.processDeleteWarrants(event) + case EventTypeResetWarrants: + err = client.initialize() + case EventTypeShutdown: + log.Fatal("Shutdown event received. Shutting down.") + } + + if err != nil { + log.Println(errors.Wrapf(err, "error processing event %s.", event.Event)) + } +} + +func (client *Client) processSetWarrants(event *sse.Event) error { + var warrants WarrantSet + err := json.Unmarshal(event.Data, &warrants) + if err != nil { + return errors.Wrapf(err, "invalid event data %s", event.Data) + } + + for w, count := range warrants { + var i uint16 = 0 + for ; i < count; i++ { + err := client.config.Repository.Incr(w) + if err != nil { + return errors.Wrapf(err, "error setting warrant %s in cache", w) + } + } + } + + return nil +} + +func (client *Client) processDeleteWarrants(event *sse.Event) error { + var warrants WarrantSet + err := json.Unmarshal(event.Data, &warrants) + if err != nil { + return errors.Wrapf(err, "invalid event data %s", event.Data) + } + + for w, count := range warrants { + var i uint16 = 0 + for ; i < count; i++ { + err := client.config.Repository.Decr(w) + if err != nil { + return errors.Wrapf(err, "error removing warrant %s from cache", w) + } + } + } + + return nil +} + +func (client *Client) restart(c *sse.Client) { + log.Printf("Disconnected from %s.", client.config.StreamingEndpoint) + client.config.Repository.SetReady(false) + + log.Println("Attempting to reconnect...") + err := client.Run() + if err != nil { + log.Fatal(errors.Wrap(err, "error restarting client")) + } +} + +func reconnectNotify(err error, d time.Duration) { + log.Println("Unable to connect.") + log.Println(err) + log.Printf("Retrying in %s", d) +} diff --git a/cmd/edge-agent/Makefile b/cmd/edge-agent/Makefile new file mode 100644 index 0000000..a4d18ec --- /dev/null +++ b/cmd/edge-agent/Makefile @@ -0,0 +1,19 @@ +NAME = edge-agent +BUILD_PATH = bin/$(NAME) +GOENV = GOARCH=amd64 GOOS=linux CGO_ENABLED=0 +GOCMD = go +GOBUILD = $(GOCMD) build -v -o $(BUILD_PATH) + +.PHONY: clean +clean: + rm -f $(BUILD_PATH) + +.PHONY: dev +dev: clean + $(GOCMD) get + $(GOBUILD) main.go + +.PHONY: build +build: clean + $(GOCMD) get + $(GOENV) $(GOBUILD) -ldflags="-s -w" main.go diff --git a/cmd/edge-agent/main.go b/cmd/edge-agent/main.go new file mode 100644 index 0000000..96fe695 --- /dev/null +++ b/cmd/edge-agent/main.go @@ -0,0 +1,113 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "errors" + "log" + "os" + + "github.com/warrant-dev/edge" + + "github.com/spf13/viper" +) + +const ( + PropertyApiEndpoint = "API_ENDPOINT" + PropertyApiKey = "API_KEY" + PropertyDatastore = "DATASTORE" + PropertyRedisHostname = "REDIS_HOSTNAME" + PropertyRedisPassword = "REDIS_PASSWORD" + PropertyRedisPort = "REDIS_PORT" + PropertyStreamingEndpoint = "STREAMING_ENDPOINT" + PropertyUpdateStrategy = "UPDATE_STRATEGY" + PropertyReadOnly = "READ_ONLY" +) + +var ErrInvalidDatastoreType = errors.New("invalid datastore type") + +func main() { + viper.SetConfigName("agent") + viper.SetConfigType("properties") + viper.AddConfigPath(".") + viper.SetDefault(PropertyApiKey, os.Getenv(PropertyApiKey)) + viper.SetDefault(PropertyApiEndpoint, os.Getenv(PropertyApiEndpoint)) + viper.SetDefault(PropertyUpdateStrategy, os.Getenv(PropertyUpdateStrategy)) + viper.SetDefault(PropertyStreamingEndpoint, os.Getenv(PropertyStreamingEndpoint)) + viper.SetDefault(PropertyDatastore, os.Getenv(PropertyDatastore)) + viper.SetDefault(PropertyRedisHostname, os.Getenv(PropertyRedisHostname)) + viper.SetDefault(PropertyRedisPort, os.Getenv(PropertyRedisPort)) + viper.SetDefault(PropertyRedisPassword, os.Getenv(PropertyRedisPassword)) + viper.SetDefault(PropertyReadOnly, os.Getenv(PropertyReadOnly)) + + if err := viper.ReadInConfig(); err != nil { + if errors.Is(err, viper.ConfigFileNotFoundError{}) { + log.Fatal(err) + } + } + + var repo edge.IRepository + var err error + switch viper.GetString(PropertyDatastore) { + case "": + repo = edge.NewMemoryRepository() + case edge.DatastoreMemory: + repo = edge.NewMemoryRepository() + case edge.DatastoreRedis: + repo, err = edge.NewRedisRepository(edge.RedisRepositoryConfig{ + Hostname: viper.GetString(PropertyRedisHostname), + Password: viper.GetString(PropertyRedisPassword), + Port: viper.GetString(PropertyRedisPort), + }) + if err != nil { + log.Fatal(err) + } + default: + log.Fatal(ErrInvalidDatastoreType) + } + + // initialize and start client + if !viper.GetBool(PropertyReadOnly) { + log.Println("Starting edge agent") + client, err := edge.NewClient(edge.ClientConfig{ + ApiKey: viper.GetString(PropertyApiKey), + ApiEndpoint: viper.GetString(PropertyApiEndpoint), + StreamingEndpoint: viper.GetString(PropertyStreamingEndpoint), + UpdateStrategy: viper.GetString(PropertyUpdateStrategy), + Repository: repo, + }) + if err != nil { + log.Fatal(err) + } + + go func() { + log.Fatal(client.Run()) + }() + } else { + log.Println("Starting edge agent in read-only mode") + } + + // initialize and start server + server, err := edge.NewServer(edge.ServerConfig{ + Port: 3000, + ApiKey: viper.GetString(PropertyApiKey), + Repository: repo, + }) + if err != nil { + log.Fatal(err) + } + + log.Fatal(server.Run()) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..82c9a24 --- /dev/null +++ b/errors.go @@ -0,0 +1,41 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package edge + +import ( + "net/http" + + "github.com/warrant-dev/warrant/pkg/service" +) + +const ( + ErrorCacheNotReady = "cache_not_ready" +) + +// CacheNotReady type +type CacheNotReady struct { + *service.GenericError +} + +func NewCacheNotReady() *CacheNotReady { + return &CacheNotReady{ + GenericError: service.NewGenericError( + "CacheNotReady", + ErrorCacheNotReady, + http.StatusServiceUnavailable, + "Edge cache not ready", + ), + } +} diff --git a/go.mod b/go.mod index 566fb23..5089773 100644 --- a/go.mod +++ b/go.mod @@ -3,25 +3,45 @@ module github.com/warrant-dev/edge go 1.21 require ( - github.com/go-playground/validator/v10 v10.15.5 github.com/go-redis/redis v6.15.9+incompatible + github.com/pkg/errors v0.9.1 github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc github.com/spf13/viper v1.17.0 + github.com/warrant-dev/warrant v0.59.0 gopkg.in/cenkalti/backoff.v1 v1.1.0 ) require ( + github.com/antonmedv/expr v1.15.3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.15.5 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + github.com/golang-migrate/migrate/v4 v4.16.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-github/v39 v39.2.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/leodido/go-urn v1.2.4 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.18.1 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/rs/zerolog v1.31.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -34,8 +54,11 @@ require ( golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 87a7c4d..eca0113 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,14 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/antonmedv/expr v1.15.3 h1:q3hOJZNvLvhqE8OHBs1cFRdbXFNKuA+bHmRaI+AmRmI= +github.com/antonmedv/expr v1.15.3/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -46,10 +52,21 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= +github.com/dhui/dktest v0.3.16/go.mod h1:gYaA3LRmM8Z4vJl2MA0THIigJoZrwOansEOsp+kqxp0= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= +github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -77,7 +94,17 @@ github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7N github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= +github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -105,6 +132,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -117,8 +146,13 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvto3aSnQ= +github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -135,9 +169,18 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -145,6 +188,8 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -158,10 +203,25 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -176,8 +236,13 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -189,10 +254,16 @@ github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNr github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= @@ -217,6 +288,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/warrant-dev/warrant v0.59.0 h1:9j3muW0RKqwzxVtADbJhfIgvI8f7FdB6n3Yh8laYicw= +github.com/warrant-dev/warrant v0.59.0/go.mod h1:W1Gr2XTlRc0q0Lz/SVJmbGo2qYAsSHXGaABYME70Xas= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -237,6 +310,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= @@ -275,6 +349,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -322,6 +398,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -373,7 +451,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -438,6 +519,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -467,6 +550,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -532,6 +616,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/edge/client.go b/internal/edge/client.go deleted file mode 100644 index b8deda3..0000000 --- a/internal/edge/client.go +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2023 Forerunner Labs, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package edge - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "time" - - "github.com/r3labs/sse" - "github.com/warrant-dev/edge/internal/datastore" - "github.com/warrant-dev/edge/internal/warrant" - "gopkg.in/cenkalti/backoff.v1" -) - -const EVENT_SET_WARRANTS = "set_warrants" -const EVENT_DEL_WARRANTS = "del_warrants" -const EVENT_RESET_WARRANTS = "reset_warrants" -const EVENT_SHUTDOWN = "shutdown" - -type ClientConfig struct { - ApiKey string - ApiEndpoint string - StreamingEndpoint string - Repository datastore.IRepository -} - -type Client struct { - config ClientConfig - sseClient *sse.Client -} - -func NewClient(config ClientConfig) *Client { - sseClient := sse.NewClient(fmt.Sprintf("%s/events", config.StreamingEndpoint)) - sseClient.Headers["Authorization"] = fmt.Sprintf("ApiKey %s", config.ApiKey) - sseClient.ReconnectStrategy = backoff.WithMaxTries(backoff.NewExponentialBackOff(), 10) - sseClient.ReconnectNotify = reconnectNotify - - return &Client{ - config: config, - sseClient: sseClient, - } -} - -func (client *Client) Run() { - log.Println("Intializing edge client...") - err := client.initialize() - if err != nil { - log.Println("Unable to initialize edge client.") - log.Println(err) - log.Fatal("Shutting down.") - } - - log.Println("Edge client initialized.") - log.Printf("Connecting to %s", client.config.StreamingEndpoint) - err = client.connect() - if err != nil { - log.Println("Unable to connect.") - log.Println(err) - log.Fatal("Shutting down.") - } -} - -func (client *Client) initialize() error { - client.config.Repository.SetReady(false) - client.config.Repository.Clear() - - resp, err := client.makeRequest("GET", "/expand", nil) - if err != nil { - return err - } - - respStatus := resp.StatusCode - if respStatus < 200 || respStatus >= 400 { - msg, err := io.ReadAll(resp.Body) - errMsg := "" - if err == nil { - errMsg = string(msg) - } - return fmt.Errorf("HTTP %d %s", respStatus, errMsg) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("error reading response %w", err) - } - - var warrants warrant.WarrantSet - err = json.Unmarshal([]byte(body), &warrants) - if err != nil { - return fmt.Errorf("invalid response from server %w", err) - } - - for warrant, count := range warrants { - err := client.config.Repository.Set(warrant, count) - if err != nil { - return fmt.Errorf("unable to set warrant %s", warrant) - } - } - - client.config.Repository.SetReady(true) - return nil -} - -func (client *Client) connect() error { - client.sseClient.OnDisconnect(client.restart) - err := client.sseClient.Subscribe(client.config.ApiKey, client.processEvent) - if err != nil { - return err - } - - return nil -} - -func (client *Client) makeRequest(method string, requestUri string, payload interface{}) (*http.Response, error) { - postBody, err := json.Marshal(payload) - if err != nil { - return nil, err - } - - requestBody := bytes.NewBuffer(postBody) - req, err := http.NewRequest(method, fmt.Sprintf("%s%s", client.config.ApiEndpoint, requestUri), requestBody) - if err != nil { - return nil, fmt.Errorf("unable to create request %w", err) - } - - req.Header.Add("Authorization", fmt.Sprintf("ApiKey %s", client.config.ApiKey)) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf("error making request %w", err) - } - - return resp, nil -} - -func (client *Client) processEvent(event *sse.Event) { - var err error - switch string(event.Event) { - case EVENT_SET_WARRANTS: - err = client.processSetWarrants(event) - case EVENT_DEL_WARRANTS: - err = client.processDelWarrants(event) - case EVENT_RESET_WARRANTS: - err = client.initialize() - case EVENT_SHUTDOWN: - log.Fatal("Shutdown event received. Shutting down.") - } - - if err != nil { - log.Printf("unable to process event %s.", event.Event) - log.Println(err) - } -} - -func (client *Client) processSetWarrants(event *sse.Event) error { - var warrants warrant.WarrantSet - err := json.Unmarshal([]byte(event.Data), &warrants) - if err != nil { - log.Printf("Invalid event data %s", event.Data) - return err - } - - for warrant, count := range warrants { - var i uint16 = 0 - for ; i < count; i++ { - err := client.config.Repository.Incr(warrant) - if err != nil { - return fmt.Errorf("unable to incr cache for warrant %s %w", warrant, err) - } - } - } - - return nil -} - -func (client *Client) processDelWarrants(event *sse.Event) error { - var warrants warrant.WarrantSet - err := json.Unmarshal([]byte(event.Data), &warrants) - if err != nil { - log.Printf("Invalid event data %s", event.Data) - return err - } - - for warrant, count := range warrants { - var i uint16 = 0 - for ; i < count; i++ { - err := client.config.Repository.Decr(warrant) - if err != nil { - return fmt.Errorf("unable to decr cache for warrant %s %w", warrant, err) - } - } - } - - return nil -} - -func (client *Client) restart(c *sse.Client) { - log.Printf("Disconnected from %s.", client.config.StreamingEndpoint) - client.config.Repository.SetReady(false) - - log.Println("Attempting to reconnect...") - client.Run() -} - -func reconnectNotify(err error, d time.Duration) { - log.Println("Unable to connect.") - log.Println(err) - log.Printf("Retrying in %s", d) -} diff --git a/internal/edge/errors.go b/internal/edge/errors.go deleted file mode 100644 index e1b5cb6..0000000 --- a/internal/edge/errors.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2023 Forerunner Labs, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package edge - -import ( - "fmt" - "net/http" -) - -const ( - ErrorCacheNotReady = "cache_not_ready" - ErrorInternalError = "internal_error" - ErrorInvalidRequest = "invalid_request" - ErrorInvalidParameter = "invalid_parameter" - ErrorMissingRequiredParameter = "missing_required_parameter" - ErrorUnauthorized = "unauthorized" -) - -type GenericError struct { - Tag string `json:"-"` - Code string `json:"code"` - Status int `json:"-"` - Message string `json:"message"` -} - -type Error interface { - GetTag() string - GetStatus() int -} - -func (err *GenericError) GetTag() string { - return err.Tag -} - -func (err *GenericError) GetStatus() int { - return err.Status -} - -func (err *GenericError) Error() string { - return fmt.Sprintf("%s: %s", err.GetTag(), err.Message) -} - -func NewGenericError(tag string, code string, status int, msg string) *GenericError { - return &GenericError{ - Tag: tag, - Code: code, - Status: status, - Message: msg, - } -} - -// CacheNotReady type -type CacheNotReady struct { - *GenericError -} - -func NewCacheNotReady() *CacheNotReady { - return &CacheNotReady{ - GenericError: NewGenericError( - "CacheNotReady", - ErrorCacheNotReady, - http.StatusServiceUnavailable, - "Edge cache not ready", - ), - } -} - -// InternalError type -type InternalError struct { - *GenericError -} - -func NewInternalError(msg string) *InternalError { - return &InternalError{ - GenericError: NewGenericError( - "InternalError", - ErrorInternalError, - http.StatusInternalServerError, - msg, - ), - } -} - -// InvalidRequestError type -type InvalidRequestError struct { - *GenericError -} - -func NewInvalidRequestError(msg string) *InvalidRequestError { - return &InvalidRequestError{ - GenericError: NewGenericError( - "InvalidRequestError", - ErrorInvalidRequest, - http.StatusBadRequest, - msg, - ), - } -} - -// InvalidParameterError type -type InvalidParameterError struct { - *GenericError - Parameter string `json:"parameter"` -} - -func NewInvalidParameterError(paramName string, msg string) *InvalidParameterError { - return &InvalidParameterError{ - GenericError: NewGenericError( - "InvalidParameterError", - ErrorInvalidParameter, - http.StatusBadRequest, - msg, - ), - Parameter: paramName, - } -} - -func (err *InvalidParameterError) Error() string { - return fmt.Sprintf("%s: Invalid parameter %s, %s", err.GetTag(), err.Parameter, err.Message) -} - -// MissingRequiredParameterError type -type MissingRequiredParameterError struct { - *GenericError - Parameter string `json:"parameter"` -} - -func NewMissingRequiredParameterError(parameterName string) *MissingRequiredParameterError { - return &MissingRequiredParameterError{ - GenericError: NewGenericError( - "MissingRequiredParameterError", - ErrorMissingRequiredParameter, - http.StatusBadRequest, - fmt.Sprintf("Missing required parameter %s", parameterName), - ), - Parameter: parameterName, - } -} diff --git a/internal/edge/json.go b/internal/edge/json.go deleted file mode 100644 index 33e6109..0000000 --- a/internal/edge/json.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2023 Forerunner Labs, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package edge - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "reflect" - "strings" - - "github.com/go-playground/validator/v10" -) - -var validate *validator.Validate - -func init() { - validate = validator.New() -} - -// SendJSONResponse sends a JSON response with the given body -func SendJSONResponse(res http.ResponseWriter, body interface{}) { - res.Header().Set("Content-type", "application/json") - res.WriteHeader(http.StatusOK) - json.NewEncoder(res).Encode(body) -} - -// SendErrorResponse sends a JSON error response with the given error -func SendErrorResponse(res http.ResponseWriter, err error) { - // log the error message - log.Println(err) - - apiError, ok := err.(Error) - status := http.StatusInternalServerError - if ok { - status = apiError.GetStatus() - } else { - apiError = NewInternalError("Internal Server Error") - } - - res.Header().Set("Content-type", "application/json") - res.WriteHeader(status) - json.NewEncoder(res).Encode(apiError) -} - -func ParseJSONBody(body io.Reader, obj interface{}) error { - reflectVal := reflect.ValueOf(obj) - if reflectVal.Kind() != reflect.Ptr { - log.Println("Second argument to ParseJSONBody must be a reference") - return NewInternalError("Internal server error") - } - - err := json.NewDecoder(body).Decode(&obj) - if err != nil { - switch err.(type) { - case *json.UnmarshalTypeError: - jsonError := err.(*json.UnmarshalTypeError) - return NewInvalidParameterError(jsonError.Field, fmt.Sprintf("must be %s", primitiveTypeToDisplayName(jsonError.Type))) - default: - return NewInvalidRequestError("Invalid request body") - } - } - - err = validate.Struct(obj) - if err != nil { - for _, err := range err.(validator.ValidationErrors) { - objType := reflect.ValueOf(obj).Elem().Type() - invalidField, fieldFound := objType.FieldByName(err.Field()) - if !fieldFound { - return NewInvalidRequestError("Invalid request body") - } - - fieldName := invalidField.Tag.Get("json") - validationRules := make(map[string]string) - validationRulesParts := strings.Split(invalidField.Tag.Get("validate"), ",") - for _, validationRulesPart := range validationRulesParts { - ruleParts := strings.Split(validationRulesPart, "=") - if len(ruleParts) > 1 { - validationRules[ruleParts[0]] = ruleParts[1] - } - } - - ruleName := err.Tag() - switch ruleName { - case "max": - return NewInvalidParameterError(fieldName, fmt.Sprintf("must be less than %s", validationRules[ruleName])) - case "min": - return NewInvalidParameterError(fieldName, fmt.Sprintf("must be greater than %s", validationRules[ruleName])) - case "required": - return NewMissingRequiredParameterError(fieldName) - default: - return NewInvalidRequestError("Invalid request body") - } - } - } - - return nil -} - -func primitiveTypeToDisplayName(primitiveType reflect.Type) string { - switch fmt.Sprint(primitiveType) { - case "bool": - return "true or false" - case "string": - return "a string" - case "int": - return "a number" - case "int8": - return "a number" - case "int16": - return "a number" - case "int32": - return "a number" - case "int64": - return "a number" - case "uint": - return "a number" - case "uint8": - return "a number" - case "uint16": - return "a number" - case "uint32": - return "a number" - case "uint64": - return "a number" - case "uintptr": - return "a number" - case "float32": - return "a decimal" - case "float64": - return "a decimal" - default: - return fmt.Sprintf("type %s", primitiveType) - } -} diff --git a/internal/edge/server.go b/internal/edge/server.go deleted file mode 100644 index cb805be..0000000 --- a/internal/edge/server.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2023 Forerunner Labs, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package edge - -import ( - "fmt" - "log" - "net/http" - - "github.com/warrant-dev/edge/internal/datastore" - "github.com/warrant-dev/edge/internal/warrant" -) - -const AUTHZ_API_VERSION = "v2" - -const AUTHORIZED = "Authorized" -const NOT_AUTHORIZED = "Not Authorized" - -const OP_ANYOF = "anyOf" -const OP_ALLOF = "allOf" - -type ServerConfig struct { - ApiKey string - Port int - StoreType string - Repository datastore.IRepository -} - -type Server struct { - config ServerConfig -} - -type WarrantCheck struct { - Op string `json:"op"` - Warrants []warrant.Warrant `json:"warrants" validate:"min=1,dive"` - ConsistentRead bool `json:"consistentRead"` -} - -type WarrantCheckResponse struct { - Code int `json:"code"` - Result string `json:"result"` -} - -func NewServer(config ServerConfig) *Server { - return &Server{ - config: config, - } -} - -func (server *Server) check(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - w.WriteHeader(http.StatusNotFound) - return - } - - if !server.config.Repository.Ready() { - SendErrorResponse(w, NewCacheNotReady()) - return - } - - var warrantCheck WarrantCheck - err := ParseJSONBody(r.Body, &warrantCheck) - if err != nil { - SendErrorResponse(w, err) - return - } - - var code int - var result string - switch warrantCheck.Op { - case OP_ANYOF: - code = http.StatusUnauthorized - result = NOT_AUTHORIZED - - for _, wnt := range warrantCheck.Warrants { - match, err := server.config.Repository.Get(wnt.String()) - if err != nil { - SendErrorResponse(w, err) - return - } - - if match { - code = http.StatusOK - result = AUTHORIZED - break - } - } - case OP_ALLOF: - code = http.StatusOK - result = AUTHORIZED - - for _, wnt := range warrantCheck.Warrants { - match, err := server.config.Repository.Get(wnt.String()) - if err != nil { - SendErrorResponse(w, err) - return - } - - if !match { - code = http.StatusUnauthorized - result = NOT_AUTHORIZED - break - } - } - default: - if warrantCheck.Op != "" { - SendErrorResponse(w, NewInvalidParameterError("op", "must be one of anyOf or allOf")) - return - } - - if len(warrantCheck.Warrants) > 1 { - SendErrorResponse(w, NewInvalidParameterError("op", "must include operator when including multiple warrants")) - return - } - - match, err := server.config.Repository.Get(warrantCheck.Warrants[0].String()) - if err != nil { - SendErrorResponse(w, err) - return - } - - if match { - code = http.StatusOK - result = AUTHORIZED - } else { - code = http.StatusUnauthorized - result = NOT_AUTHORIZED - } - } - - SendJSONResponse(w, WarrantCheckResponse{ - Code: code, - Result: result, - }) -} - -func (server *Server) Run() { - mux := http.NewServeMux() - mux.Handle(fmt.Sprintf("/%s/authorize", AUTHZ_API_VERSION), loggingMiddleware(http.HandlerFunc(server.check))) - - log.Printf("Edge server serving authz requests on port %d", server.config.Port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", server.config.Port), mux)) -} diff --git a/internal/warrant/warrant.go b/internal/warrant/warrant.go deleted file mode 100644 index 56a068b..0000000 --- a/internal/warrant/warrant.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2023 Forerunner Labs, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package warrant - -import ( - "fmt" - "log" -) - -type Subject struct { - ObjectType string `json:"objectType" validate:"required"` - ObjectId string `json:"objectId" validate:"required"` -} - -func (subject Subject) String() string { - return fmt.Sprintf("%s:%s", subject.ObjectType, subject.ObjectId) -} - -type Warrant struct { - ObjectType string `json:"objectType" validate:"required"` - ObjectId string `json:"objectId" validate:"required"` - Relation string `json:"relation" validate:"required"` - Subject Subject `json:"subject" validate:"required"` -} - -func (warrant Warrant) String() string { - return fmt.Sprintf("%s:%s#%s@%s", warrant.ObjectType, warrant.ObjectId, warrant.Relation, warrant.Subject) -} - -// A set of Warrant strings -type WarrantSet map[string]uint16 - -func (set WarrantSet) Add(key string) { - log.Printf("Adding warrant %s to WarrantSet", key) - - if count, ok := set[key]; ok { - set[key] = count + 1 - return - } - - set[key] = 1 -} diff --git a/main.go b/main.go deleted file mode 100644 index 8a0f221..0000000 --- a/main.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2023 Forerunner Labs, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "log" - "os" - - "github.com/spf13/viper" - "github.com/warrant-dev/edge/internal/datastore" - "github.com/warrant-dev/edge/internal/edge" -) - -const PROPERTY_API_ENDPOINT = "API_ENDPOINT" -const PROPERTY_API_KEY = "API_KEY" -const PROPERTY_DATASTORE = "DATASTORE" -const PROPERTY_REDIS_HOSTNAME = "REDIS_HOSTNAME" -const PROPERTY_REDIS_PASSWORD = "REDIS_PASSWORD" -const PROPERTY_REDIS_PORT = "REDIS_PORT" -const PROPERTY_STREAMING_ENDPOINT = "STREAMING_ENDPOINT" - -func main() { - viper.SetConfigName("agent") - viper.SetConfigType("properties") - viper.AddConfigPath(".") - viper.SetDefault(PROPERTY_API_KEY, os.Getenv(PROPERTY_API_KEY)) - viper.SetDefault(PROPERTY_API_ENDPOINT, os.Getenv(PROPERTY_API_ENDPOINT)) - viper.SetDefault(PROPERTY_STREAMING_ENDPOINT, os.Getenv(PROPERTY_STREAMING_ENDPOINT)) - viper.SetDefault(PROPERTY_DATASTORE, os.Getenv(PROPERTY_DATASTORE)) - viper.SetDefault(PROPERTY_REDIS_HOSTNAME, os.Getenv(PROPERTY_REDIS_HOSTNAME)) - viper.SetDefault(PROPERTY_REDIS_PORT, os.Getenv(PROPERTY_REDIS_PORT)) - viper.SetDefault(PROPERTY_REDIS_PASSWORD, os.Getenv(PROPERTY_REDIS_PASSWORD)) - - if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); !ok { - log.Fatal(err) - } - } - - var repo datastore.IRepository - apiKey := viper.GetString(PROPERTY_API_KEY) - storeType := viper.GetString(PROPERTY_DATASTORE) - switch storeType { - case "": - repo = datastore.NewMemoryRepository() - case datastore.DATASTORE_MEMORY: - repo = datastore.NewMemoryRepository() - case datastore.DATASTORE_REDIS: - repo = datastore.NewRedisRepository(datastore.RedisRepositoryConfig{ - Hostname: viper.GetString(PROPERTY_REDIS_HOSTNAME), - Password: viper.GetString(PROPERTY_REDIS_PASSWORD), - Port: viper.GetString(PROPERTY_REDIS_PORT), - }) - default: - log.Fatal("Invalid storeType provided") - } - - edgeServer := edge.NewServer(edge.ServerConfig{ - Port: 3000, - ApiKey: apiKey, - Repository: repo, - }) - - apiEndpoint := "https://api.warrant.dev/v1" - if viper.GetString(PROPERTY_API_ENDPOINT) != "" { - apiEndpoint = viper.GetString(PROPERTY_API_ENDPOINT) - } - - streamingEndpoint := "https://stream.warrant.dev/v1" - if viper.GetString(PROPERTY_STREAMING_ENDPOINT) != "" { - streamingEndpoint = viper.GetString(PROPERTY_STREAMING_ENDPOINT) - } - - edgeClient := edge.NewClient(edge.ClientConfig{ - ApiKey: apiKey, - ApiEndpoint: apiEndpoint, - StreamingEndpoint: streamingEndpoint, - Repository: repo, - }) - - go edgeClient.Run() - edgeServer.Run() -} diff --git a/internal/datastore/memory.go b/memory.go similarity index 79% rename from internal/datastore/memory.go rename to memory.go index 925931c..733bf9d 100644 --- a/internal/datastore/memory.go +++ b/memory.go @@ -12,23 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package datastore +package edge import ( - "log" "sync" ) -const DATASTORE_MEMORY = "memory" - type WarrantCache struct { hashCount map[string]uint16 - lock sync.Mutex + lock sync.RWMutex } func (cache *WarrantCache) Contains(key string) bool { - cache.lock.Lock() - defer cache.lock.Unlock() + cache.lock.RLock() + defer cache.lock.RUnlock() _, ok := cache.hashCount[key] return ok @@ -67,9 +64,28 @@ func (cache *WarrantCache) Decr(key string) { } } -func (cache *WarrantCache) Clear() { - log.Printf("Clearing cache") +func (cache *WarrantCache) Update(warrants WarrantSet) error { + cache.lock.Lock() + defer cache.lock.Unlock() + + // iterate over existing records and remove any that no longer exist + for key := range cache.hashCount { + if warrants.Has(key) { + cache.Set(key, warrants.Get(key)) + } else { + delete(cache.hashCount, key) + } + } + // add any newly created records + for key, value := range warrants { + cache.Set(key, value) + } + + return nil +} + +func (cache *WarrantCache) Clear() { cache.lock.Lock() defer cache.lock.Unlock() cache.hashCount = make(map[string]uint16) @@ -77,7 +93,7 @@ func (cache *WarrantCache) Clear() { type MemoryRepository struct { cache *WarrantCache - lock sync.Mutex + lock sync.RWMutex ready bool } @@ -109,6 +125,10 @@ func (repo *MemoryRepository) Decr(key string) error { return nil } +func (repo *MemoryRepository) Update(warrants WarrantSet) error { + return repo.cache.Update(warrants) +} + func (repo *MemoryRepository) Clear() error { repo.cache.hashCount = make(map[string]uint16) return nil @@ -122,8 +142,8 @@ func (repo *MemoryRepository) SetReady(newReady bool) { } func (repo *MemoryRepository) Ready() bool { - repo.lock.Lock() - defer repo.lock.Unlock() + repo.lock.RLock() + defer repo.lock.RUnlock() return repo.ready } diff --git a/internal/edge/middleware.go b/middleware.go similarity index 100% rename from internal/edge/middleware.go rename to middleware.go diff --git a/internal/datastore/redis.go b/redis.go similarity index 52% rename from internal/datastore/redis.go rename to redis.go index 24b2956..5c49522 100644 --- a/internal/datastore/redis.go +++ b/redis.go @@ -12,18 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package datastore +package edge import ( "fmt" - "log" + "strings" "sync" + "github.com/pkg/errors" + "github.com/go-redis/redis" ) -const DATASTORE_REDIS = "redis" - type RedisRepositoryConfig struct { Hostname string Password string @@ -36,11 +36,10 @@ type RedisRepository struct { lock sync.Mutex } -func NewRedisRepository(config RedisRepositoryConfig) *RedisRepository { - var connectionString string - +func NewRedisRepository(config RedisRepositoryConfig) (*RedisRepository, error) { + hostname := config.Hostname if config.Hostname == "" { - log.Fatal("Must set redis hostname") + hostname = "127.0.0.1" } port := config.Port @@ -48,77 +47,73 @@ func NewRedisRepository(config RedisRepositoryConfig) *RedisRepository { port = "6379" } + var connectionString string if config.Password != "" { - connectionString = fmt.Sprintf("rediss://default:%s@%s:%s/1", config.Password, config.Hostname, port) + connectionString = fmt.Sprintf("rediss://default:%s@%s:%s/1", config.Password, hostname, port) } else { - connectionString = fmt.Sprintf("redis://default:%s@%s:%s/1", config.Password, config.Hostname, port) + connectionString = fmt.Sprintf("redis://default:%s@%s:%s/1", config.Password, hostname, port) } opt, err := redis.ParseURL(connectionString) if err != nil { - panic(err) + return nil, errors.Wrap(err, fmt.Sprintf("invalid connection string %s", connectionString)) } rdb := redis.NewClient(opt) _, err = rdb.Ping().Result() if err != nil { - log.Println("Unable to ping redis. Check your credentials.") - log.Println("Shutting down.") - log.Fatal(err) + return nil, errors.Wrap(err, "Unable to ping redis. Check your credentials.") } return &RedisRepository{ client: rdb, ready: true, - } + }, nil } func (repo *RedisRepository) Get(key string) (bool, error) { - namespacedKey := fmt.Sprintf("%s:%s", repo.getNamespace(), key) - _, err := repo.client.Get(namespacedKey).Result() + _, err := repo.client.Get(repo.keyWithNamespace(key)).Result() if err == redis.Nil { return false, nil } if err != nil { - return false, err + return false, errors.Wrap(err, "error getting key from redis") } return true, nil } func (repo *RedisRepository) Set(key string, count uint16) error { - namespacedKey := fmt.Sprintf("%s:%s", repo.getNamespace(), key) - _, err := repo.client.Set(namespacedKey, count, 0).Result() + _, err := repo.client.Set(repo.keyWithNamespace(key), count, 0).Result() if err != nil { - return err + return errors.Wrap(err, "error setting key in redis") } return nil } func (repo *RedisRepository) Incr(key string) error { - namespacedKey := fmt.Sprintf("%s:%s", repo.getNamespace(), key) - _, err := repo.client.Incr(namespacedKey).Result() + _, err := repo.client.Incr(repo.keyWithNamespace(key)).Result() if err != nil { - return err + return errors.Wrap(err, "error incrementing key in redis") } return nil } func (repo *RedisRepository) Decr(key string) error { - namespacedKey := fmt.Sprintf("%s:%s", repo.getNamespace(), key) + namespacedKey := repo.keyWithNamespace(key) maxRetries := 10 decrementAndRemoveFunc := func(tx *redis.Tx) error { count, err := repo.client.Decr(namespacedKey).Result() if err != nil { - return err + return errors.Wrap(err, "error decrementing key in redis") } if count <= 0 { _, err = repo.client.Del(namespacedKey).Result() if err != nil { - return err + return errors.Wrap(err, "error deleting key from redis") } } @@ -131,13 +126,48 @@ func (repo *RedisRepository) Decr(key string) error { // Retry continue } else if err != nil { - return err + return errors.Wrap(err, "error calling watch in redis") } return nil } - return fmt.Errorf("unable to acquire lock to remove %s from cache", key) + return errors.New(fmt.Sprintf("unable to acquire lock to remove %s from cache", key)) +} + +func (repo *RedisRepository) Update(warrants WarrantSet) error { + prefix := fmt.Sprintf("%s*", repo.getNamespace()) + iter := repo.client.Scan(0, prefix, 0).Iterator() + + // iterate over existing records and remove any that no longer exist + for iter.Next() { + keyWithNamespace := iter.Val() + keyWithoutNamespace := repo.keyWithoutNamespace(keyWithNamespace) + if warrants.Has(keyWithoutNamespace) { + err := repo.Set(keyWithoutNamespace, warrants.Get(keyWithoutNamespace)) + if err != nil { + return errors.Wrap(err, "error updating key in redis") + } + } else { + err := repo.client.Del(keyWithNamespace).Err() + if err != nil { + return errors.Wrap(err, "error deleting key from redis") + } + } + } + if err := iter.Err(); err != nil { + return errors.Wrap(err, "error iterating over keys in redis") + } + + // add any newly created records + for keyWithoutNamespace, count := range warrants { + err := repo.Set(keyWithoutNamespace, count) + if err != nil { + return errors.Wrap(err, "error updating key in redis") + } + } + + return nil } func (repo *RedisRepository) Clear() error { @@ -148,11 +178,11 @@ func (repo *RedisRepository) Clear() error { key := iter.Val() err := repo.client.Del(key).Err() if err != nil { - return err + return errors.Wrap(err, "error deleting key from redis") } } if err := iter.Err(); err != nil { - return err + return errors.Wrap(err, "error iterating over keys in redis") } return nil @@ -175,3 +205,11 @@ func (repo *RedisRepository) Ready() bool { func (repo *RedisRepository) getNamespace() string { return "warrant" } + +func (repo *RedisRepository) keyWithNamespace(key string) string { + return fmt.Sprintf("%s:%s", repo.getNamespace(), key) +} + +func (repo *RedisRepository) keyWithoutNamespace(key string) string { + return strings.TrimPrefix(key, fmt.Sprintf("%s:", repo.getNamespace())) +} diff --git a/internal/datastore/repository.go b/repository.go similarity index 87% rename from internal/datastore/repository.go rename to repository.go index 8338d3b..2a8429d 100644 --- a/internal/datastore/repository.go +++ b/repository.go @@ -12,13 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package datastore +package edge + +const ( + DatastoreMemory = "memory" + DatastoreRedis = "redis" +) type IRepository interface { Get(key string) (bool, error) Set(key string, count uint16) error Incr(key string) error Decr(key string) error + Update(warrants WarrantSet) error Clear() error SetReady(isReady bool) Ready() bool diff --git a/server.go b/server.go new file mode 100644 index 0000000..d83ba8c --- /dev/null +++ b/server.go @@ -0,0 +1,158 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package edge + +import ( + "fmt" + "log" + "net/http" + + check "github.com/warrant-dev/warrant/pkg/authz/check" + "github.com/warrant-dev/warrant/pkg/service" +) + +const ( + ApiVersion = "v2" + OpAnyOf = "anyOf" + OpAllOf = "allOf" + ResultAuthorized = "Authorized" + ResultNotAuthorized = "Not Authorized" +) + +type ServerConfig struct { + ApiKey string + Port int + Repository IRepository +} + +type Server struct { + config ServerConfig +} + +func NewServer(config ServerConfig) (*Server, error) { + return &Server{ + config: config, + }, nil +} + +func (server *Server) health(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusNotFound) + return + } + + if server.config.Repository.Ready() { + w.WriteHeader(http.StatusOK) + return + } + + w.WriteHeader(http.StatusInternalServerError) +} + +func (server *Server) check(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusNotFound) + return + } + + if !server.config.Repository.Ready() { + service.SendErrorResponse(w, NewCacheNotReady()) + return + } + + var checkManySpec check.CheckManySpec + err := service.ParseJSONBody(r.Body, &checkManySpec) + if err != nil { + service.SendErrorResponse(w, err) + } + + var code int64 + var result string + switch checkManySpec.Op { + case OpAnyOf: + code = http.StatusForbidden + result = ResultNotAuthorized + + for _, wnt := range checkManySpec.Warrants { + match, err := server.config.Repository.Get(wnt.String()) + if err != nil { + service.SendErrorResponse(w, err) + return + } + + if match { + code = http.StatusOK + result = ResultAuthorized + break + } + } + case OpAllOf: + code = http.StatusOK + result = ResultAuthorized + + for _, wnt := range checkManySpec.Warrants { + match, err := server.config.Repository.Get(wnt.String()) + if err != nil { + service.SendErrorResponse(w, err) + return + } + + if !match { + code = http.StatusForbidden + result = ResultNotAuthorized + break + } + } + default: + if checkManySpec.Op != "" { + service.SendErrorResponse(w, service.NewInvalidParameterError("op", "must be one of anyOf or allOf")) + return + } + + if len(checkManySpec.Warrants) > 1 { + service.SendErrorResponse(w, service.NewInvalidParameterError("op", "must include operator when including multiple warrants")) + return + } + + match, err := server.config.Repository.Get(checkManySpec.Warrants[0].String()) + if err != nil { + service.SendErrorResponse(w, err) + return + } + + if match { + code = http.StatusOK + result = ResultAuthorized + } else { + code = http.StatusForbidden + result = ResultNotAuthorized + } + } + + service.SendJSONResponse(w, check.CheckResultSpec{ + Code: code, + Result: result, + }) +} + +func (server *Server) Run() error { + mux := http.NewServeMux() + mux.Handle("/health", loggingMiddleware(http.HandlerFunc(server.health))) + mux.Handle(fmt.Sprintf("/%s/authorize", ApiVersion), loggingMiddleware(http.HandlerFunc(server.check))) + mux.Handle(fmt.Sprintf("/%s/check", ApiVersion), loggingMiddleware(http.HandlerFunc(server.check))) + + log.Printf("Edge agent ready to serve authz requests on port %d", server.config.Port) + return http.ListenAndServe(fmt.Sprintf(":%d", server.config.Port), mux) +} diff --git a/warrant.go b/warrant.go new file mode 100644 index 0000000..fe2d3a4 --- /dev/null +++ b/warrant.go @@ -0,0 +1,46 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package edge + +import "fmt" + +type WarrantSet map[string]uint16 + +func (set WarrantSet) Add(key string) { + if count, ok := set[key]; ok { + set[key] = count + 1 + return + } + + set[key] = 1 +} + +func (set WarrantSet) Has(key string) bool { + _, exists := set[key] + return exists +} + +func (set WarrantSet) Get(key string) uint16 { + return set[key] +} + +func (set WarrantSet) String() string { + str := "" + for key, value := range set { + str = fmt.Sprintf("%s\n%s => %d", str, key, value) + } + + return str +}