diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..dbb4454 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d415ffb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +MamContract +vendor/ +.idea/ + +*.iml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c125323 --- /dev/null +++ b/Makefile @@ -0,0 +1,86 @@ +# Set an output prefix, which is the local directory if not specified +PREFIX?=$(shell pwd) +.DEFAULT_GOAL := build + +DOCKER_IMAGE_NAME := MamContract +# Setup name variables for the package/tool +NAME := MamContract +BIN_NAME := MamContract +PKG := github.com/kratisto/mam-contract + +# GO env vars +ifeq ($(GOPATH),) + GOPATH:=~/go +endif +GO=$(firstword $(subst :, ,$(GOPATH))) + +.PHONY: ensure-vendor +ensure-vendor: ## Get all vendor dependencies + @echo "+ $@" + dep ensure + +.PHONY: update-vendor +update-vendor: ## Get all vendor dependencies + @echo "+ $@" + dep ensure -update + +.PHONY: clean +clean: local-clean ## Clean your generated files + @echo "+ $@" + docker image rm workScheduler:snapshot || true + +.PHONY: local-build +local-build: local-clean local-format ## Build locally the binary + @echo "+ $@" + go build -o $(GO)/bin/$(BIN_NAME) . + +.PHONY: local-clean +local-clean: ## Cleanup locally any build binaries or packages + @echo "+ $@" + @$(RM) $(BIN_NAME) + +.PHONY: local-format +local-format: ## format locally all files + @echo "+ $@" + gofmt -s -l -w . + +.PHONY: build +build: sources-image ## Build the docker image with application binary + @echo "+ $@" + docker build --no-cache \ + -f containers/Dockerfile \ + --build-arg SOURCES_IMAGE=workScheduler-sources:snapshot \ + -t workScheduler:snapshot . + +GIT_CREDENTIALS?=$(shell cat ~/.git-credentials 2> /dev/null) +.PHONY: sources-image +sources-image: ## Generate a Docker image with only the sources + @echo "+ $@" + docker build -t workScheduler-sources:snapshot -f containers/Dockerfile.sources . + + +.PHONY: local-run-dependencies +local-run-dependencies: ## Run the dependencies of the server (launch the containers/docker-compose.local.yml) + @echo "+ $@" + @docker-compose -p $(DOCKER_IMAGE_NAME)-uuid -f containers/docker-compose.local.yml down || true; + @docker-compose -p $(DOCKER_IMAGE_NAME)-uuid -f containers/docker-compose.local.yml pull; + @docker-compose -p $(DOCKER_IMAGE_NAME)-uuid -f containers/docker-compose.local.yml up -d --build + +.PHONY: local-run-golang +local-run-golang: ## Build the server and run it + @echo "+ $@" + BUILD_GOLANG_CMD=local-build \ + LAUNCH_GOLANG_CMD="$(GO)/bin/$(BIN_NAME)" \ + $(MAKE) local-launch-golang + +.PHONY: local-launch-golang +local-launch-golang: ## Build the server and run it + @echo "+ $@" + PID=`ps -ax | egrep "\b$(BIN_NAME)"| cut -d " " -f 1`; \ + kill $$PID || true + $(MAKE) $(BUILD_GOLANG_CMD) + DB_PORT=`docker-compose -p $(DOCKER_IMAGE_NAME)-uuid -f containers/docker-compose.local.yml port database 5432 | cut -f2 -d':'`; \ + $(LAUNCH_GOLANG_CMD) serve --loglevel debug --logformat text --postgreshost localhost:$$DB_PORT + +.PHONY: local-run +local-run: local-run-dependencies local-run-golang ## Run the server with its dependencies diff --git a/README.md b/README.md index 2d2534e..302f76c 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# work-scheduler \ No newline at end of file +# mam-contract diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..422de3f --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "fmt" + "github.com/kratisto/mam-contract/configuration" + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "os" +) + +const ( + // Log + parameterLogLevel = "loglevel" + parameterLogFormat = "logformat" + + defaultLogLevel = "debug" + defaultLogFormat = "text" + + // Mock + parameterMock = "mock" + + defaultMock = false + + // Router + parameterPort = "port" + defaultPort = 8080 + + // DATABASE + parameterPostgresDBName = "postgresdbname" + defaultPostgresDBName = "postgres" + parameterPostgresDBSchema = "postgresdbschema" + defaultPostgresDBSchema = "workScheduler" + parameterPostgresHost = "postgreshost" + defaultPostgresHost = "database" + parameterPostgresUser = "postgresuser" + defaultPostgresUser = "postgres" + parameterPostgresPwd = "postgrespwd" + defaultPostgresPwd = "password" +) + +var ( + config = &configuration.Config{} + + cfgFile string + // GITHASH : Stores the git revision to be displayed + GITHASH string + // VERSION : Stores the binary version to be displayed + VERSION string + + // rootCmd represents the base command when called without any subcommands + rootCmd = &cobra.Command{ + Use: "workScheduler", + Short: "workScheduler", + Version: fmt.Sprintf("%s (%s)", VERSION, GITHASH), + } +) + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + rootCmd.AddCommand(serveCmd) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.workScheduler.yaml)") + +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Search config in home directory with name ".workScheduler" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".workScheduler") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } + + config.Mock = viper.GetBool(parameterMock) + + config.Port = viper.GetInt(parameterPort) + + config.LogLevel = viper.GetString(parameterLogLevel) + config.LogFormat = viper.GetString(parameterLogFormat) + + config.PostgresDBName = viper.GetString(parameterPostgresDBName) + config.PostgresDBSchema = viper.GetString(parameterPostgresDBSchema) + config.PostgresHost = viper.GetString(parameterPostgresHost) + config.PostgresUser = viper.GetString(parameterPostgresUser) + config.PostgresPwd = viper.GetString(parameterPostgresPwd) + + config.Version = VERSION + +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..5e41f7e --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "github.com/kratisto/mam-contract/internal/ginserver" + "github.com/kratisto/mam-contract/internal/health" + "github.com/kratisto/mam-contract/internal/utils" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// serveCmd represents the serve command +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Serve endpoints", + PreRun: initServeBindingsFlags, + Run: func(cmd *cobra.Command, args []string) { + initConfig() + utils.InitLogger(config.LogLevel, config.LogFormat) + logrus. + WithField(parameterLogLevel, config.LogLevel). + WithField(parameterLogFormat, config.LogFormat). + WithField(parameterPort, config.Port). + WithField(parameterPostgresHost, config.PostgresHost). + Warn("Configuration") + injector := &utils.Injector{} + ginserver.Setup(injector, config) + health.Setup(injector) + ginserver.Start(injector) + + }, +} + +func init() { + serveCmd.Flags().String(parameterLogLevel, defaultLogLevel, "Use this flag to set the logging level") + serveCmd.Flags().String(parameterLogFormat, defaultLogFormat, "Use this flag to set the logging format") + serveCmd.Flags().Bool(parameterMock, defaultMock, "Use this flag to mock external services") + serveCmd.Flags().Int(parameterPort, defaultPort, "Use this flag to set the listening port of the api") + serveCmd.Flags().String(parameterPostgresDBName, defaultPostgresDBName, "Use this flag to set database name") + serveCmd.Flags().String(parameterPostgresDBSchema, defaultPostgresDBSchema, "Use this flag to set database schema name") + serveCmd.Flags().String(parameterPostgresHost, defaultPostgresHost, "Use this flag to set database host") + serveCmd.Flags().String(parameterPostgresUser, defaultPostgresUser, "Use this flag to set database user name") + serveCmd.Flags().String(parameterPostgresPwd, defaultPostgresPwd, "Use this flag to set database user password") +} + +func initServeBindingsFlags(cmd *cobra.Command, args []string) { + viper.BindPFlag(parameterLogLevel, cmd.Flags().Lookup(parameterLogLevel)) + viper.BindPFlag(parameterLogFormat, cmd.Flags().Lookup(parameterLogFormat)) + viper.BindPFlag(parameterMock, cmd.Flags().Lookup(parameterMock)) + viper.BindPFlag(parameterPort, cmd.Flags().Lookup(parameterPort)) + viper.BindPFlag(parameterPostgresDBName, cmd.Flags().Lookup(parameterPostgresDBName)) + viper.BindPFlag(parameterPostgresDBSchema, cmd.Flags().Lookup(parameterPostgresDBSchema)) + viper.BindPFlag(parameterPostgresHost, cmd.Flags().Lookup(parameterPostgresHost)) + viper.BindPFlag(parameterPostgresUser, cmd.Flags().Lookup(parameterPostgresUser)) + viper.BindPFlag(parameterPostgresPwd, cmd.Flags().Lookup(parameterPostgresPwd)) +} diff --git a/configuration/config.go b/configuration/config.go new file mode 100644 index 0000000..ee58e52 --- /dev/null +++ b/configuration/config.go @@ -0,0 +1,19 @@ +package configuration + +// Config holds all the configuration of the application +type Config struct { + Mock bool + + Port int + + LogLevel string + LogFormat string + + PostgresDBName string + PostgresDBSchema string + PostgresHost string + PostgresUser string + PostgresPwd string + + Version string +} diff --git a/containers/Dockerfile b/containers/Dockerfile new file mode 100644 index 0000000..3712824 --- /dev/null +++ b/containers/Dockerfile @@ -0,0 +1,17 @@ +# build stage +ARG SOURCES_IMAGE +FROM $SOURCES_IMAGE as builder + +RUN make local-build + +# final stage +FROM alpine:3.7 +RUN apk --no-cache add ca-certificates + + +WORKDIR /app +COPY --from=builder /go/bin/MamContract . +EXPOSE 8080 + +ENTRYPOINT ["/app/MamContract"] +CMD ["serve", "--logformat", "json", "--loglevel", "debug"] diff --git a/containers/Dockerfile.sources b/containers/Dockerfile.sources new file mode 100644 index 0000000..441adc3 --- /dev/null +++ b/containers/Dockerfile.sources @@ -0,0 +1,17 @@ +FROM golang:1.11.2-alpine3.8 + +# Remove the cache to avoid warning message during `apk info` +RUN rm -rf /var/cache/apk/* && \ + rm -rf /tmp/* +RUN apk update + +# GO and PATH env variables already set in golang image +# to reduce download time +RUN apk --no-cache add -U make git musl-dev gcc + +RUN wget https://raw.githubusercontent.com/golang/dep/master/install.sh && sh install.sh + +WORKDIR $GOPATH/src/github.com/kratisto/MamContract +ADD . . + +RUN make ensure-vendor diff --git a/containers/docker-compose.local.yml b/containers/docker-compose.local.yml new file mode 100644 index 0000000..c67541c --- /dev/null +++ b/containers/docker-compose.local.yml @@ -0,0 +1,37 @@ +version: "2.1" + +services: + database: + image: postgres:10-alpine + ports: + - "5432" + environment: + - POSTGRES_PASSWORD=password + healthcheck: + test: ["CMD-SHELL", "psql -U postgres -c 'SELECT 1;'"] + interval: 10s + timeout: 30s + retries: 3 + + liquibase: + image: kilna/liquibase-mysql + depends_on: + database: + condition: service_healthy + links: + - database + environment: + - LIQUIBASE_DATABASE=workScheduler + - LIQUIBASE_HOST=database + - LIQUIBASE_USERNAME=postgres + - LIQUIBASE_PASSWORD=password + - LIQUIBASE_CHANGELOG=/changelogs/changelog-master.xml + - LIQUIBASE_CLASSPATH=/changelogs/postgresql-42.2.5.jar + - LIQUIBASE_DRIVER=org.postgresql.Driver + - LIQUIBASE_URL=jdbc:postgresql://database:5432/workScheduler + volumes: + - $PWD/liquibase/changelogs/:/workspace + command: + - liquibase + - --defaultsFile=/workspace/liquibase.properties + - update diff --git a/dao/postgres.go b/dao/postgres.go new file mode 100644 index 0000000..1801096 --- /dev/null +++ b/dao/postgres.go @@ -0,0 +1,47 @@ +package dao + +import ( + "database/sql" + "fmt" + "github.com/kratisto/mam-contract/configuration" + + _ "github.com/lib/pq" // Required to use the driver with database/sql +) + +// Database is the interface for DB access +type Database interface { + Ping() error +} + +// DatabasePostgreSQL is the implementation of the Database interface for PostgreSQL +type DatabasePostgreSQL struct { + session *sql.DB + schema string +} + +// NewDatabasePostgreSQL returns an instance of the database +func NewDatabasePostgreSQL(config *configuration.Config) Database { + var ( + dbSession *sql.DB + err error + ) + + connectionURI := fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", config.PostgresUser, config.PostgresPwd, config.PostgresHost, config.PostgresDBName) + dbSession, err = sql.Open("postgres", connectionURI) + if err != nil { + return nil + } + + err = dbSession.Ping() + if err != nil { + return nil + } + return &DatabasePostgreSQL{session: dbSession, schema: config.PostgresDBSchema} +} + +// Ping doesn't use db.Ping because it doesn't Ping after the first time +// https://stackoverflow.com/a/41619206/3853913 +func (db *DatabasePostgreSQL) Ping() error { + _, err := db.session.Query("SELECT WHERE 1=0") + return err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..71d32de --- /dev/null +++ b/go.mod @@ -0,0 +1,58 @@ +module github.com/kratisto/mam-contract + +go 1.21 + +require ( + github.com/gin-contrib/cors v1.4.0 + github.com/gin-gonic/gin v1.8.1 + github.com/lib/pq v1.0.0 + github.com/mitchellh/go-homedir v1.1.0 + github.com/sirupsen/logrus v1.2.0 + github.com/spf13/cobra v0.0.3 + github.com/spf13/viper v1.3.2 + google.golang.org/api v0.3.0 + gopkg.in/go-playground/validator.v9 v9.31.0 +) + +require ( + cloud.google.com/go v0.34.0 // indirect + github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.10.0 // indirect + github.com/goccy/go-json v0.9.7 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/hashicorp/golang-lru v0.5.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/magiconair/properties v1.8.0 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.3.0 // indirect + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/spf13/pflag v1.0.3 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + go.opencensus.io v0.19.2 // indirect + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 // indirect + golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + golang.org/x/text v0.3.6 // indirect + google.golang.org/appengine v1.4.0 // indirect + google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 // indirect + google.golang.org/grpc v1.19.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0ca35b7 --- /dev/null +++ b/go.sum @@ -0,0 +1,262 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= +github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +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/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A= +go.opencensus.io v0.19.2 h1:ZZpq6xI6kv/LuE/5s5UQvBU5vMjvRnPb8PvJrIntAnc= +go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +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= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU= +google.golang.org/api v0.3.0 h1:UIJY20OEo3+tK5MBlcdx37kmdH6EnRjGkW78mc6+EeA= +google.golang.org/api v0.3.0/go.mod h1:IuvZyQh8jgscv8qWfQ4ABd8m7hEudgBFM/EdhA3BnXw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= +gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/ginserver/logger.go b/internal/ginserver/logger.go new file mode 100755 index 0000000..a4614ec --- /dev/null +++ b/internal/ginserver/logger.go @@ -0,0 +1,71 @@ +package ginserver + +import ( + "github.com/kratisto/mam-contract/internal/utils" + "math/rand" + "time" + + "github.com/gin-gonic/gin" +) + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) +} + +func GetLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + correlationID := c.Request.Header.Get(utils.HeaderNameCorrelationID) + if correlationID == "" { + correlationID = randStringBytesMaskImprSrc(30) + c.Writer.Header().Set(utils.HeaderNameCorrelationID, correlationID) + } + + logger := utils.GetLogger() + logEntry := logger.WithField(utils.HeaderNameCorrelationID, correlationID) + + c.Set(utils.ContextKeyLogger, logEntry) + } +} + +func GetHTTPLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + + utils.GetLoggerFromCtx(c). + WithField("method", c.Request.Method). + WithField("url", c.Request.RequestURI). + WithField("from", c.ClientIP()). + Info("start handling HTTP request") + + c.Next() + d := time.Since(start) + + utils.GetLoggerFromCtx(c). + WithField("status", c.Writer.Status()). + WithField("duration", d.String()). + Info("end handling HTTP request") + } +} diff --git a/internal/ginserver/oauth_token.go b/internal/ginserver/oauth_token.go new file mode 100644 index 0000000..aa63619 --- /dev/null +++ b/internal/ginserver/oauth_token.go @@ -0,0 +1,40 @@ +package ginserver + +import ( + "fmt" + "github.com/kratisto/mam-contract/internal/storage/model" + "github.com/kratisto/mam-contract/internal/utils" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "google.golang.org/api/oauth2/v1" +) + +func ValidateOAuthToken(c *gin.Context) { + authorizationHeader := c.GetHeader("Authorization") + authorizationHeaderSplitted := strings.Split(authorizationHeader, " ") + if len(authorizationHeaderSplitted) != 2 { + utils.JSONError(c.Writer, model.ErrBadRequestFormat) + c.Abort() + return + } + + oauth2Service, err := oauth2.New(&http.Client{}) + if oauth2Service == nil { + fmt.Println(err) + utils.JSONError(c.Writer, model.ErrInternalServer) + c.Abort() + return + } + tokenInfoCall := oauth2Service.Tokeninfo() + tokenInfoCall.IdToken(authorizationHeaderSplitted[1]) + token, err := tokenInfoCall.Do() + if err != nil { + utils.GetLogger().WithError(err).Error(err) + utils.JSONError(c.Writer, model.ErrBadRequestFormat) + c.Abort() + return + } + c.Set("googleUserId", token.UserId) +} diff --git a/internal/ginserver/setup.go b/internal/ginserver/setup.go new file mode 100644 index 0000000..c44c600 --- /dev/null +++ b/internal/ginserver/setup.go @@ -0,0 +1,45 @@ +package ginserver + +import ( + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/kratisto/mam-contract/configuration" + "github.com/kratisto/mam-contract/internal/utils" + "time" +) + +var ( + routerInjectorKey = "ROUTER" + + SecuredRouterInjectorKey = "SECURED_ROUTER" + UnsecuredRouterInjectorKey = "UNSECURED_ROUTER" +) + +func Setup(injector *utils.Injector, config *configuration.Config) { + + gin.SetMode(gin.ReleaseMode) + + router := gin.New() + router.HandleMethodNotAllowed = true + + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{"http://localhost:8080/", "http://localhost:8080", "http://localhost:19006"}, + AllowMethods: []string{"*"}, + AllowHeaders: []string{"*"}, + ExposeHeaders: []string{"*"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + router.Use(gin.Recovery()) + router.Use(GetLoggerMiddleware()) + router.Use(GetHTTPLoggerMiddleware()) + public := router.Group("/") + injector.Set(UnsecuredRouterInjectorKey, public) + + securedUserRoute := public.Group("/users") + securedUserRoute.Use(ValidateOAuthToken) + + injector.Set(SecuredRouterInjectorKey, securedUserRoute) + injector.Set(routerInjectorKey, router) + +} diff --git a/internal/ginserver/start.go b/internal/ginserver/start.go new file mode 100644 index 0000000..1f22c58 --- /dev/null +++ b/internal/ginserver/start.go @@ -0,0 +1,38 @@ +package ginserver + +import ( + "github.com/gin-gonic/gin" + "github.com/kratisto/mam-contract/internal/utils" + "log" + "net/http" + "os" + "os/signal" + "syscall" +) + +func Start(injector *utils.Injector) { + router := utils.Get[*gin.Engine](injector, routerInjectorKey) + + srv := &http.Server{ + Addr: ":8080", + Handler: router, + } + + go func() { + // service connections + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }() + + // Wait for interrupt signal to gracefully shutdown the server with + // a timeout of 5 seconds. + quit := make(chan os.Signal) + // kill (no param) default send syscanll.SIGTERM + // kill -2 is syscall.SIGINT + // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutdown Server ...") + +} diff --git a/internal/health/handler.go b/internal/health/handler.go new file mode 100644 index 0000000..c8d31ca --- /dev/null +++ b/internal/health/handler.go @@ -0,0 +1,27 @@ +package health + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// @openapi:path +// /_health: +// +// get: +// tags: +// - "Monitoring" +// summary: Health check +// description: Health check +// responses: +// 200: +// description: "Health response" +// content: +// application/json: +// schema: +// $ref: "#/components/schemas/Health" +func GetHealth(c *gin.Context) { + health := &Health{Alive: true} + c.JSON(http.StatusOK, health) +} diff --git a/internal/health/model.go b/internal/health/model.go new file mode 100644 index 0000000..45c10d2 --- /dev/null +++ b/internal/health/model.go @@ -0,0 +1,8 @@ +package health + +// Health struct +// @openapi:schema +type Health struct { + Alive bool `json:"alive"` + Version string `json:"version"` +} diff --git a/internal/health/setup.go b/internal/health/setup.go new file mode 100644 index 0000000..f662441 --- /dev/null +++ b/internal/health/setup.go @@ -0,0 +1,15 @@ +package health + +import ( + "github.com/gin-gonic/gin" + "github.com/kratisto/mam-contract/internal/ginserver" + "github.com/kratisto/mam-contract/internal/utils" + "net/http" +) + +func Setup(injector *utils.Injector) { + publicRoute := utils.Get[*gin.RouterGroup](injector, ginserver.UnsecuredRouterInjectorKey) + //TODO add secure auth + publicRoute.Handle(http.MethodGet, "/health", GetHealth) + +} diff --git a/internal/storage/dao/database_error.go b/internal/storage/dao/database_error.go new file mode 100755 index 0000000..160f190 --- /dev/null +++ b/internal/storage/dao/database_error.go @@ -0,0 +1,32 @@ +package dao + +import ( + "fmt" +) + +type Type int + +const ( + ErrTypeNotFound Type = iota + ErrTypeDuplicate + ErrTypeForeignKeyViolation +) + +type Error struct { + Cause error + Type Type +} + +func NewDAOError(t Type, cause error) error { + return &Error{ + Type: t, + Cause: cause, + } +} + +func (e *Error) Error() string { + if e.Cause != nil { + return fmt.Sprintf("Type %d: %s", e.Type, e.Cause.Error()) + } + return fmt.Sprintf("Type %d: no cause given", e.Type) +} diff --git a/internal/storage/dao/postgresql/database_postgresql.go b/internal/storage/dao/postgresql/database_postgresql.go new file mode 100755 index 0000000..1c6c140 --- /dev/null +++ b/internal/storage/dao/postgresql/database_postgresql.go @@ -0,0 +1,43 @@ +package postgresql + +import ( + "database/sql" + "fmt" + "github.com/kratisto/mam-contract/internal/storage/dao" + "github.com/kratisto/mam-contract/internal/utils" + + "github.com/lib/pq" +) + +const ( + pgCodeUniqueViolation = "23505" + pgCodeForeingKeyViolation = "23503" +) + +func HandlePgError(e *pq.Error) error { + if e.Code == pgCodeUniqueViolation { + return dao.NewDAOError(dao.ErrTypeDuplicate, e) + } + + if e.Code == pgCodeForeingKeyViolation { + return dao.NewDAOError(dao.ErrTypeForeignKeyViolation, e) + } + return e +} + +type DatabasePostgreSQL struct { + Session *sql.DB +} + +func NewDatabasePostgreSQL(connectionURI string) *DatabasePostgreSQL { + db, err := sql.Open("postgres", connectionURI) + if err != nil { + utils.GetLogger().WithError(err).Fatal("Unable to get a connection to the postgres db") + } + err = db.Ping() + if err != nil { + fmt.Println(err) + utils.GetLogger().WithError(err).Fatal("Unable to ping the postgres db") + } + return &DatabasePostgreSQL{Session: db} +} diff --git a/internal/storage/dao/postgresql/setup.go b/internal/storage/dao/postgresql/setup.go new file mode 100644 index 0000000..1e5a1fa --- /dev/null +++ b/internal/storage/dao/postgresql/setup.go @@ -0,0 +1,13 @@ +package postgresql + +import ( + "github.com/kratisto/mam-contract/internal/utils" +) + +var DatabaseKey = "POSTGRES" + +func Setup(injector *utils.Injector, connectionUri string) { + database := NewDatabasePostgreSQL(connectionUri) + + injector.Set(DatabaseKey, database) +} diff --git a/internal/storage/model/error.go b/internal/storage/model/error.go new file mode 100644 index 0000000..2099261 --- /dev/null +++ b/internal/storage/model/error.go @@ -0,0 +1,68 @@ +package model + +import ( + "fmt" + "net/http" + + "github.com/lib/pq" +) + +var ( + // 400 + ErrBadRequestFormat = APIError{ + Type: "bad_format", + HTTPCode: http.StatusBadRequest, + Description: "unable to read request body, please check that the json is valid", + } + ErrDataValidation = APIError{ + Type: "data_validation", + HTTPCode: http.StatusBadRequest, + Description: "the data are not valid", + } + + // 404 + ErrNotFound = APIError{ + Type: "not_found", + HTTPCode: http.StatusNotFound, + } + + // 40x + ErrAlreadyExists = APIError{ + Type: "already_exists", + HTTPCode: http.StatusConflict, + } + + // 50x + ErrInternalServer = APIError{ + Type: "internal_server_error", + HTTPCode: http.StatusInternalServerError, + } +) + +type APIError struct { + HTTPCode int `json:"-"` + Type string `json:"error"` + Description string `json:"error_description"` + Details []FieldError `json:"error_details,omitempty"` + Headers map[string][]string `json:"-"` +} + +type FieldError struct { + Field string `json:"field"` + Constraint string `json:"constraint"` + Description string `json:"description"` +} + +func (e *APIError) Error() string { + return fmt.Sprintf("error : %d, %s, %s, %v", e.HTTPCode, e.Type, e.Description, e.Details) +} + +func FromPostgresError(err *pq.Error) *APIError { + return &APIError{ + HTTPCode: 0, + Type: "", + Description: "", + Details: nil, + Headers: nil, + } +} diff --git a/internal/storage/validators/error.go b/internal/storage/validators/error.go new file mode 100755 index 0000000..209845a --- /dev/null +++ b/internal/storage/validators/error.go @@ -0,0 +1,44 @@ +package validators + +import ( + "fmt" + "github.com/kratisto/mam-contract/internal/storage/model" + "github.com/kratisto/mam-contract/internal/utils" + "regexp" + "strings" + + "gopkg.in/go-playground/validator.v9" +) + +var regexpValidatorNamespacePrefix = regexp.MustCompile(`^\w+\.`) + +func NewDataValidationAPIError(err error) model.APIError { + apiErr := model.ErrDataValidation + if err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + utils.GetLogger().WithError(err).WithField("templateAPIErr", apiErr).Error("InvalidValidationError") + } else { + for _, e := range err.(validator.ValidationErrors) { + reason := e.Tag() + if _, ok := CustomValidators[e.Tag()]; ok { + reason = truncatingSprintf(CustomValidators[e.Tag()].Message, e.Param()) + } + + namespaceWithoutStructName := regexpValidatorNamespacePrefix.ReplaceAllString(e.Namespace(), "") + fe := model.FieldError{ + Field: namespaceWithoutStructName, + Constraint: e.Tag(), + Description: reason, + } + apiErr.Details = append(apiErr.Details, fe) + } + } + } + return apiErr +} + +// truncatingSprintf is used as fmt.Sprintf but allow to truncate the additional parameters given when there is more parameters than %v in str +func truncatingSprintf(str string, args ...interface{}) string { + n := strings.Count(str, "%v") + return fmt.Sprintf(str, args[:n]...) +} diff --git a/internal/storage/validators/validators.go b/internal/storage/validators/validators.go new file mode 100755 index 0000000..d547f97 --- /dev/null +++ b/internal/storage/validators/validators.go @@ -0,0 +1,34 @@ +package validators + +import ( + "context" + "strings" + + "gopkg.in/go-playground/validator.v9" +) + +var ( + CustomValidators = map[string]customValidator{ + "enum": { + Message: "This field should be in: %v", + Validator: validateEnum, + }, + "required": { + Message: "This field is required and cannot be empty", + }, + } +) + +type customValidator struct { + Message string + Validator validator.FuncCtx +} + +func validateEnum(ctx context.Context, fl validator.FieldLevel) bool { + for _, v := range strings.Split(fl.Param(), " ") { + if v == fl.Field().String() { + return true + } + } + return false +} diff --git a/internal/utils/headers.go b/internal/utils/headers.go new file mode 100755 index 0000000..d4bba29 --- /dev/null +++ b/internal/utils/headers.go @@ -0,0 +1,8 @@ +package utils + +const ( + HeaderNameContentType = "content-type" + HeaderNameCorrelationID = "correlationID" + + HeaderValueApplicationJSONUTF8 = "application/json; charset=UTF-8" +) diff --git a/internal/utils/injector.go b/internal/utils/injector.go new file mode 100644 index 0000000..c337622 --- /dev/null +++ b/internal/utils/injector.go @@ -0,0 +1,29 @@ +package utils + +import "fmt" + +type Injector struct { + content map[string]any +} + +func (i *Injector) Get(key string) any { + val, ok := i.content[key] + if !ok { + panic(fmt.Sprintf("Can't get key %s from injector", key)) + } + return val +} +func Get[T any](i *Injector, key string) T { + return i.Get(key).(T) +} + +func (i *Injector) Set(key string, content any) { + if i.content == nil { + i.content = map[string]any{} + } + _, ok := i.content[key] + if ok { + panic(fmt.Sprintf("Key %s already have content", key)) + } + i.content[key] = content +} diff --git a/internal/utils/logger.go b/internal/utils/logger.go new file mode 100755 index 0000000..cd08509 --- /dev/null +++ b/internal/utils/logger.go @@ -0,0 +1,74 @@ +package utils + +import ( + "io" + "os" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +const ( + LogFormatText = "text" + LogFormatJSON = "json" + + ContextKeyLogger = "logger" +) + +var ( + logLevel = logrus.DebugLevel + logFormat logrus.Formatter = &logrus.TextFormatter{} + logOut io.Writer +) + +func InitLogger(ll, lf string) { + logLevel = parseLogrusLevel(ll) + logrus.SetLevel(logLevel) + + logFormat = parseLogrusFormat(lf) + logrus.SetFormatter(logFormat) + + logOut = os.Stdout + logrus.SetOutput(logOut) +} + +func GetLoggerFromCtx(c *gin.Context) *logrus.Entry { + if logger, ok := c.Get(ContextKeyLogger); ok { + logEntry, assertionOk := logger.(*logrus.Entry) + if assertionOk { + return logEntry + } + } + return logrus.NewEntry(GetLogger()) +} + +func GetLogger() *logrus.Logger { + logger := logrus.New() + logger.Formatter = logFormat + logger.Level = logLevel + logger.Out = logOut + return logger +} + +func parseLogrusLevel(logLevelStr string) logrus.Level { + logLevel, err := logrus.ParseLevel(logLevelStr) + if err != nil { + logrus.WithError(err).Errorf("error while parsing log level. %v is set as default.", logLevel) + logLevel = logrus.DebugLevel + } + return logLevel +} + +func parseLogrusFormat(logFormatStr string) logrus.Formatter { + var formatter logrus.Formatter + switch logFormatStr { + case LogFormatText: + formatter = &logrus.TextFormatter{ForceColors: true, FullTimestamp: true} + case LogFormatJSON: + formatter = &logrus.JSONFormatter{} + default: + logrus.Errorf("error while parsing log format. %v is set as default.", formatter) + formatter = &logrus.TextFormatter{ForceColors: true, FullTimestamp: true} + } + return formatter +} diff --git a/internal/utils/randomizer/rand.go b/internal/utils/randomizer/rand.go new file mode 100644 index 0000000..9446871 --- /dev/null +++ b/internal/utils/randomizer/rand.go @@ -0,0 +1,45 @@ +package randomizer + +import ( + "fmt" + "math/rand" +) + +type Random interface { + Int() int +} + +func init() {} + +type rndGenerator func(n int) int + +type Randomizer struct { + fn rndGenerator +} + +func NewRandomizer(fn rndGenerator) *Randomizer { + return &Randomizer{fn: fn} +} + +func (r *Randomizer) Int(n int) int { + return r.fn(n) +} + +func Rand(rand int) int { + return randGen(rand) +} +func realRand(n int) int { return int(rand.Intn(n)) } +func fakeRand(n int) func(numb int) int { + return func(numb int) int { + if n >= numb { + panic(fmt.Sprintf("%d Should not be superior of %d", n, numb)) + } + return n + } +} + +var randGen = NewRandomizer(realRand).Int + +func FakeRandomizer(n int) { + randGen = NewRandomizer(fakeRand(n)).Int +} diff --git a/internal/utils/responses.go b/internal/utils/responses.go new file mode 100755 index 0000000..58f4404 --- /dev/null +++ b/internal/utils/responses.go @@ -0,0 +1,31 @@ +package utils + +import ( + "encoding/json" + "github.com/kratisto/mam-contract/internal/storage/model" + "net/http" +) + +func JSON(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set(HeaderNameContentType, HeaderValueApplicationJSONUTF8) + w.WriteHeader(status) + if data != nil { + json.NewEncoder(w).Encode(data) + } +} + +func JSONError(w http.ResponseWriter, e model.APIError) { + if e.Headers != nil { + for k, headers := range e.Headers { + for _, headerValue := range headers { + w.Header().Add(k, headerValue) + } + } + } + JSON(w, e.HTTPCode, e) +} + +func JSONErrorWithMessage(w http.ResponseWriter, e model.APIError, message string) { + e.Description = message + JSONError(w, e) +} diff --git a/internal/utils/validator/setup.go b/internal/utils/validator/setup.go new file mode 100644 index 0000000..9d4d1ed --- /dev/null +++ b/internal/utils/validator/setup.go @@ -0,0 +1,12 @@ +package validator + +import ( + "github.com/kratisto/mam-contract/internal/utils" +) + +const ValidatorInjectorKey = "VALIDATOR" + +func Setup(injector *utils.Injector) { + + injector.Set(ValidatorInjectorKey, newValidator()) +} diff --git a/internal/utils/validator/validator.go b/internal/utils/validator/validator.go new file mode 100644 index 0000000..7b8ed43 --- /dev/null +++ b/internal/utils/validator/validator.go @@ -0,0 +1,28 @@ +package validator + +import ( + "github.com/kratisto/mam-contract/internal/storage/validators" + "gopkg.in/go-playground/validator.v9" + "reflect" + "strings" +) + +func newValidator() *validator.Validate { + va := validator.New() + + va.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := strings.SplitN(fld.Tag.Get("json"), ",", 2) + if len(name) < 1 { + return "" + } + return name[0] + }) + + for k, v := range validators.CustomValidators { + if v.Validator != nil { + va.RegisterValidationCtx(k, v.Validator) + } + } + + return va +} diff --git a/liquibase/changelogs/changelog-master.xml b/liquibase/changelogs/changelog-master.xml new file mode 100644 index 0000000..4f75bdb --- /dev/null +++ b/liquibase/changelogs/changelog-master.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/liquibase/changelogs/changesets/create-user-table.xml b/liquibase/changelogs/changesets/create-user-table.xml new file mode 100644 index 0000000..d7d4fb3 --- /dev/null +++ b/liquibase/changelogs/changesets/create-user-table.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/liquibase/changelogs/liquibase.properties b/liquibase/changelogs/liquibase.properties new file mode 100644 index 0000000..50d2f4e --- /dev/null +++ b/liquibase/changelogs/liquibase.properties @@ -0,0 +1,7 @@ +classpath: /workspace/postgresql-42.2.5.jar +driver: org.postgresql.Driver +url: jdbc:postgresql://database:5432/postgres +username: postgres +password: password +changeLogFile: changelog-master.xml +logLevel: info diff --git a/liquibase/changelogs/prepare-database.sql b/liquibase/changelogs/prepare-database.sql new file mode 100755 index 0000000..a749597 --- /dev/null +++ b/liquibase/changelogs/prepare-database.sql @@ -0,0 +1,2 @@ +CREATE SCHEMA IF NOT EXISTS workScheduler; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/main.go b/main.go new file mode 100644 index 0000000..919fc03 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/kratisto/mam-contract/cmd" + +func main() { + cmd.Execute() +}