diff --git a/.gitignore b/.gitignore index db77c52..6886fea 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ coverage.txt .devcontainer Taskfile.yaml +bin/* +docker-compose.yml +.env +tmp.sh diff --git a/Makefile b/Makefile index 284ff30..e9ca905 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,16 @@ pkgs := $(shell go list ./...) run := . count := 1 timeout := 120m +BINARY_NAME := knuu + +build: + go build -o bin/$(BINARY_NAME) -v ./cmd + +# docker: +# docker build -t $(BINARY_NAME) . + +run: build + ./bin/$(BINARY_NAME) api -l debug ## help: Show this help message help: Makefile diff --git a/cmd/api/api.go b/cmd/api/api.go new file mode 100644 index 0000000..63294e0 --- /dev/null +++ b/cmd/api/api.go @@ -0,0 +1,238 @@ +package api + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "github.com/celestiaorg/knuu/internal/api/v1" + "github.com/celestiaorg/knuu/internal/api/v1/services" + "github.com/celestiaorg/knuu/internal/database" +) + +const ( + apiCmdName = "api" + + flagPort = "port" + flagAPILogLevel = "log-level" + + flagDBHost = "db.host" + flagDBUser = "db.user" + flagDBPassword = "db.password" + flagDBName = "db.name" + flagDBPort = "db.port" + + flagSecretKey = "secret-key" + flagAdminUser = "admin-user" + flagAdminPass = "admin-pass" + + flagTestsLogsPath = "tests-logs-path" + + defaultPort = 8080 + defaultLogLevel = gin.ReleaseMode + + defaultDBHost = database.DefaultHost + defaultDBUser = database.DefaultUser + defaultDBPassword = database.DefaultPassword + defaultDBName = database.DefaultDBName + defaultDBPort = database.DefaultPort + + defaultSecretKey = "secret" + defaultAdminUser = "admin" + defaultAdminPass = "admin" + + defaultLogsPath = services.DefaultTestLogsPath +) + +func NewAPICmd() *cobra.Command { + apiCmd := &cobra.Command{ + Use: apiCmdName, + Short: "Start the Knuu API server", + Long: "Start the API server to manage tests, tokens, and users.", + RunE: runAPIServer, + } + + apiCmd.Flags().IntP(flagPort, "p", defaultPort, "Port to run the API server on") + apiCmd.Flags().StringP(flagAPILogLevel, "l", defaultLogLevel, "Log level: debug | release | test") + + apiCmd.Flags().StringP(flagDBHost, "d", defaultDBHost, "Postgres database host") + apiCmd.Flags().StringP(flagDBUser, "", defaultDBUser, "Postgres database user") + apiCmd.Flags().StringP(flagDBPassword, "", defaultDBPassword, "Postgres database password") + apiCmd.Flags().StringP(flagDBName, "", defaultDBName, "Postgres database name") + apiCmd.Flags().IntP(flagDBPort, "", defaultDBPort, "Postgres database port") + + apiCmd.Flags().StringP(flagSecretKey, "", defaultSecretKey, "JWT secret key") + apiCmd.Flags().StringP(flagAdminUser, "", defaultAdminUser, "Admin username") + apiCmd.Flags().StringP(flagAdminPass, "", defaultAdminPass, "Admin password") + + apiCmd.Flags().StringP(flagTestsLogsPath, "", defaultLogsPath, "Directory to store logs of the tests") + + return apiCmd +} + +func runAPIServer(cmd *cobra.Command, args []string) error { + dbOpts, err := getDBOptions(cmd.Flags()) + if err != nil { + return fmt.Errorf("failed to get database options: %v", err) + } + + db, err := database.New(dbOpts) + if err != nil { + return fmt.Errorf("failed to connect to database: %v", err) + } + + apiOpts, err := getAPIOptions(cmd.Flags()) + if err != nil { + return fmt.Errorf("failed to get API options: %v", err) + } + + apiServer, err := api.New(context.Background(), db, apiOpts) + if err != nil { + return fmt.Errorf("failed to create API server: %v", err) + } + + handleShutdown(apiServer, db, apiOpts.Logger) + + return apiServer.Start() +} + +func getDBOptions(flags *pflag.FlagSet) (database.Options, error) { + dbHost, err := flags.GetString(flagDBHost) + if err != nil { + return database.Options{}, fmt.Errorf("failed to get database host: %v", err) + } + + dbUser, err := flags.GetString(flagDBUser) + if err != nil { + return database.Options{}, fmt.Errorf("failed to get database user: %v", err) + } + + dbPassword, err := flags.GetString(flagDBPassword) + if err != nil { + return database.Options{}, fmt.Errorf("failed to get database password: %v", err) + } + + dbName, err := flags.GetString(flagDBName) + if err != nil { + return database.Options{}, fmt.Errorf("failed to get database name: %v", err) + } + + dbPort, err := flags.GetInt(flagDBPort) + if err != nil { + return database.Options{}, fmt.Errorf("failed to get database port: %v", err) + } + + apiLogLevel, err := flags.GetString(flagAPILogLevel) + if err != nil { + return database.Options{}, fmt.Errorf("failed to get API log level: %v", err) + } + + var dbLogLevel logger.LogLevel + switch apiLogLevel { + case gin.DebugMode: + dbLogLevel = logger.Info + case gin.ReleaseMode: + dbLogLevel = logger.Error + case gin.TestMode: + dbLogLevel = logger.Info + } + + return database.Options{ + Host: dbHost, + User: dbUser, + Password: dbPassword, + DBName: dbName, + Port: dbPort, + LogLevel: dbLogLevel, + }, nil +} + +func getAPIOptions(flags *pflag.FlagSet) (api.Options, error) { + port, err := flags.GetInt(flagPort) + if err != nil { + return api.Options{}, fmt.Errorf("failed to get port: %v", err) + } + + apiLogLevel, err := flags.GetString(flagAPILogLevel) + if err != nil { + return api.Options{}, fmt.Errorf("failed to get log level: %v", err) + } + + secretKey, err := flags.GetString(flagSecretKey) + if err != nil { + return api.Options{}, fmt.Errorf("failed to get secret key: %v", err) + } + + adminUser, err := flags.GetString(flagAdminUser) + if err != nil { + return api.Options{}, fmt.Errorf("failed to get admin user: %v", err) + } + + adminPass, err := flags.GetString(flagAdminPass) + if err != nil { + return api.Options{}, fmt.Errorf("failed to get admin password: %v", err) + } + + testsLogsPath, err := flags.GetString(flagTestsLogsPath) + if err != nil { + return api.Options{}, fmt.Errorf("failed to get tests logs path: %v", err) + } + + logger := logrus.New() + logger.SetFormatter(&logrus.JSONFormatter{}) + + switch apiLogLevel { + case gin.DebugMode: + logger.SetLevel(logrus.DebugLevel) + case gin.ReleaseMode: + logger.SetLevel(logrus.ErrorLevel) + case gin.TestMode: + logger.SetLevel(logrus.InfoLevel) + } + + return api.Options{ + Port: port, + APILogMode: apiLogLevel, // gin logger (HTTP request level) + SecretKey: secretKey, + AdminUser: adminUser, + AdminPass: adminPass, + Logger: logger, // handler (application level logger) + TestServiceOptions: services.TestServiceOptions{ + TestsLogsPath: testsLogsPath, // directory to store logs of each test + Logger: logger, + }, + }, nil +} + +func handleShutdown(apiServer *api.API, db *gorm.DB, logger *logrus.Logger) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt, syscall.SIGTERM) + + sqlDB, err := db.DB() + if err != nil { + logger.Errorf("failed to get sql db: %v", err) + } + + go func() { + sig := <-quit + logger.Infof("Received signal: %v. Shutting down gracefully...", sig) + if err := sqlDB.Close(); err != nil { + logger.Errorf("failed to close sql db: %v", err) + } + logger.Info("DB connection closed") + if err := apiServer.Stop(context.Background()); err != nil { + logger.Errorf("failed to stop api server: %v", err) + } + logger.Info("API server stopped") + os.Exit(0) + }() +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..2c79e61 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "github.com/sirupsen/logrus" + + "github.com/celestiaorg/knuu/cmd/root" +) + +func main() { + if err := root.Execute(); err != nil { + logrus.WithError(err).Fatal("failed to execute command") + } +} diff --git a/cmd/root/root.go b/cmd/root/root.go new file mode 100644 index 0000000..c0f617c --- /dev/null +++ b/cmd/root/root.go @@ -0,0 +1,22 @@ +package root + +import ( + "github.com/celestiaorg/knuu/cmd/api" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "knuu", + Short: "Knuu CLI", + Long: "Knuu CLI provides commands to manage the Knuu API server and its operations.", +} + +// Execute runs the root command. +func Execute() error { + return rootCmd.Execute() +} + +func init() { + rootCmd.AddCommand(api.NewAPICmd()) +} diff --git a/go.mod b/go.mod index 5324d71..97541e7 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,20 @@ go 1.22.5 require ( github.com/celestiaorg/bittwister v0.0.0-20231213180407-65cdbaf5b8c7 - github.com/docker/docker v26.1.4+incompatible + github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 + github.com/lib/pq v1.10.9 github.com/minio/minio-go/v7 v7.0.74 + github.com/rs/cors v1.11.1 github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.29.0 gopkg.in/yaml.v2 v2.4.0 + gorm.io/driver/postgres v1.5.11 + gorm.io/gorm v1.25.12 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 k8s.io/client-go v0.30.2 @@ -17,26 +25,27 @@ require ( ) require ( - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/acroca/go-symbols v0.1.1 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cilium/ebpf v0.12.3 // indirect - github.com/containerd/log v0.1.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.5.0 // indirect - github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 // indirect - github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/frankban/quicktest v1.14.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // 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.20.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -47,42 +56,37 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/karrick/godirwalk v1.17.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/nsf/gocode v0.0.0-20230322162601-b672b49f3818 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/ramya-rao-a/go-outline v0.0.0-20210608161538-9736a4bde949 // indirect github.com/rs/xid v1.5.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/uudashr/gopkgs v1.3.2 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.26.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 // indirect - go.opentelemetry.io/otel/metric v1.26.0 // indirect - go.opentelemetry.io/otel/sdk v1.26.0 // indirect - go.opentelemetry.io/otel/trace v1.26.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.29.0 // indirect + golang.org/x/arch v0.8.0 // indirect golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect - golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.31.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sync v0.9.0 // indirect @@ -91,14 +95,10 @@ require ( golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.27.0 // indirect - golang.org/x/tools/cmd/guru v0.1.1-deprecated // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.5.1 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/go.sum b/go.sum index aa90d21..40d42ea 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,28 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -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/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -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/acroca/go-symbols v0.1.1 h1:q3IzaMNYocw/Bnc2a8jkXf0hM3+POfLoq30x8HYuaPE= -github.com/acroca/go-symbols v0.1.1/go.mod h1:RKAIDWtcELAw6/wjNJGWRYZ7QEinSWoJeJ2H5cfK6AM= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/celestiaorg/bittwister v0.0.0-20231213180407-65cdbaf5b8c7 h1:nxplQi8wrLMjhu260RuigXylC3pWoDu4OVumPHeojnk= github.com/celestiaorg/bittwister v0.0.0-20231213180407-65cdbaf5b8c7/go.mod h1:1EF5MfOxVf0WC51Gb7pJ6bcZxnXKNAf9pqWtjgPBAYc= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/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/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= -github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 h1:IPrmumsT9t5BS7XcPhgsCTlkWbYg80SEXUzDpReaU6Y= -github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11/go.mod h1:a6bNUGTbQBsY6VRHTr4h/rkOXjl244DyRD0tx3fgq4Q= -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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -47,23 +37,36 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +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.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -95,24 +98,35 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= -github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -120,111 +134,91 @@ 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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.74 h1:fTo/XlPBTSpo3BAMshlwKL5RspXRv9us5UeHEGYCFe0= github.com/minio/minio-go/v7 v7.0.74/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nsf/gocode v0.0.0-20230322162601-b672b49f3818 h1:btvxUuer0DCdhu/N5fvMxW759ASqzIsm6cF8D23TNYs= -github.com/nsf/gocode v0.0.0-20230322162601-b672b49f3818/go.mod h1:6Q8/OMaaKAgTX7/jt2bOXVDrm1eJhoNd+iwzghR7jvs= github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= -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.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= -github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/ramya-rao-a/go-outline v0.0.0-20210608161538-9736a4bde949 h1:iaD+iVf9xGfajsJp+zYrg9Lrk6gMJ6/hZHO4cYq5D5o= -github.com/ramya-rao-a/go-outline v0.0.0-20210608161538-9736a4bde949/go.mod h1:9V3eNbj9Z53yO7cKB6cSX9f0O7rYdIiuGBhjA1YsQuw= github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -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.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/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/uudashr/gopkgs v1.3.2 h1:ACme7LZyeSNIRIl9HtAA0RsT0eePUsrkHDVb2+aswhg= -github.com/uudashr/gopkgs v1.3.2/go.mod h1:MtCdKVJkxW7hNKWXPNWfpaeEp8+Ml3Q8myb4yWhn2Hg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38= -go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= -go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= -go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= -go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= -go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -235,14 +229,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -253,12 +240,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -269,40 +251,25 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -310,10 +277,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -326,16 +289,9 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= -golang.org/x/tools/cmd/guru v0.1.1-deprecated h1:WiL3pQGXG71u4N45C0eRkE2IcEMAiQdDZ2H5lGspNjM= -golang.org/x/tools/cmd/guru v0.1.1-deprecated/go.mod h1:yFb7vixnH8+ByFZ63niwlvUUxyTE/6ULZ6AiEHZwlTk= 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= @@ -347,21 +303,15 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= 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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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= @@ -373,8 +323,10 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= @@ -389,6 +341,8 @@ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7F k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/internal/api/v1/api.go b/internal/api/v1/api.go new file mode 100644 index 0000000..dc05113 --- /dev/null +++ b/internal/api/v1/api.go @@ -0,0 +1,168 @@ +package api + +import ( + "context" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/rs/cors" + "github.com/sirupsen/logrus" + "gorm.io/gorm" + + "github.com/celestiaorg/knuu/internal/api/v1/handlers" + "github.com/celestiaorg/knuu/internal/api/v1/middleware" + "github.com/celestiaorg/knuu/internal/api/v1/services" + "github.com/celestiaorg/knuu/internal/database/models" + "github.com/celestiaorg/knuu/internal/database/repos" +) + +const ( + defaultPort = 8080 + defaultLogMode = gin.ReleaseMode +) + +type apiCleanup struct { + testService *services.TestService +} + +type API struct { + router *gin.Engine + server *http.Server + cleanup apiCleanup +} + +type Options struct { + Port int + OriginAllowed string + APILogMode string // gin.DebugMode, gin.ReleaseMode(default), gin.TestMode + SecretKey string + + AdminUser string // default admin username + AdminPass string // default admin password + + Logger *logrus.Logger + TestServiceOptions services.TestServiceOptions +} + +func New(ctx context.Context, db *gorm.DB, opts Options) (*API, error) { + opts = setDefaults(opts) + gin.SetMode(opts.APILogMode) + + rt := gin.Default() + + auth := middleware.NewAuth(opts.SecretKey) + uh, err := getUserHandler(ctx, opts, db, auth, opts.Logger) + if err != nil { + return nil, err + } + + public := rt.Group("/") + { + public.POST(pathsUserLogin, uh.Login) + } + + testService, err := services.NewTestService(ctx, + repos.NewTestRepository(db), + opts.TestServiceOptions, + ) + if err != nil { + return nil, err + } + + protected := rt.Group("/", auth.AuthMiddleware()) + { + protected.POST(pathsUserRegister, auth.RequireRole(models.RoleAdmin), uh.Register) + + th := handlers.NewTestHandler(testService, opts.Logger) + protected.POST(pathsTests, th.CreateTest) + protected.GET(pathsTestDetails, th.GetTestDetails) + protected.GET(pathsTestLogs, th.GetTestLogs) + // protected.GET(pathsTestInstances, th.GetInstances) + protected.POST(pathsTestInstances, th.CreateInstance) // Need to do something about updating an instance + protected.GET(pathsTestInstanceDetails, th.GetInstance) + protected.GET(pathsTestInstanceStatus, th.GetInstanceStatus) + protected.POST(pathsTestInstanceExecute, th.ExecuteInstance) + } + + _ = protected + + a := &API{ + router: rt, + server: &http.Server{ + Addr: fmt.Sprintf(":%d", opts.Port), + Handler: handleOrigin(rt, opts.OriginAllowed), + }, + cleanup: apiCleanup{ + testService: testService, + }, + } + + if opts.APILogMode != gin.ReleaseMode { + public.GET("/", a.IndexPage) + } + + return a, nil +} + +func (a *API) Start() error { + fmt.Printf("Starting API server in %s mode on %s\n", gin.Mode(), a.server.Addr) + return a.server.ListenAndServe() +} + +func (a *API) Stop(ctx context.Context) error { + fmt.Println("Shutting down API server") + if a.cleanup.testService != nil { + err := a.cleanup.testService.Shutdown(ctx) + if err != nil { + return err + } + } + return a.server.Close() +} + +func setDefaults(opts Options) Options { + if opts.Port == 0 { + opts.Port = defaultPort + } + + if opts.APILogMode == "" { + opts.APILogMode = defaultLogMode + } + + if opts.SecretKey == "" { + opts.SecretKey = "secret" + } + + if opts.Logger == nil { + opts.Logger = logrus.New() + opts.Logger.SetFormatter(&logrus.JSONFormatter{}) + } + + return opts +} + +func handleOrigin(router *gin.Engine, originAllowed string) http.Handler { + if originAllowed == "" { + return router + } + + headersOk := []string{"X-Requested-With", "Content-Type", "Content-Length", "Accept-Encoding", "Authorization", "X-CSRF-Token"} + originsOk := []string{originAllowed} + methodsOk := []string{"GET", "HEAD", "POST", "PUT", "OPTIONS"} + + return cors.New(cors.Options{ + AllowedHeaders: headersOk, + AllowedOrigins: originsOk, + AllowedMethods: methodsOk, + }).Handler(router) +} + +func getUserHandler(ctx context.Context, opts Options, db *gorm.DB, auth *middleware.Auth, logger *logrus.Logger) (*handlers.UserHandler, error) { + us, err := services.NewUserService(ctx, opts.AdminUser, opts.AdminPass, repos.NewUserRepository(db)) + if err != nil { + return nil, err + } + + return handlers.NewUserHandler(us, auth, logger), nil +} diff --git a/internal/api/v1/handlers/errors.go b/internal/api/v1/handlers/errors.go new file mode 100644 index 0000000..e7c41d5 --- /dev/null +++ b/internal/api/v1/handlers/errors.go @@ -0,0 +1,9 @@ +package handlers + +import "github.com/celestiaorg/knuu/pkg/errors" + +type Error = errors.Error + +var ( + ErrInvalidCredentials = errors.New("InvalidCredentials", "invalid credentials") +) diff --git a/internal/api/v1/handlers/instance.go b/internal/api/v1/handlers/instance.go new file mode 100644 index 0000000..cf0674e --- /dev/null +++ b/internal/api/v1/handlers/instance.go @@ -0,0 +1,113 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + + "github.com/celestiaorg/knuu/internal/api/v1/services" +) + +func (h *TestHandler) CreateInstance(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "CreateInstance", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + user, err := getUserFromContext(c) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var input services.Instance + if err := c.ShouldBindJSON(&input); err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + input.Scope = c.Param("scope") + err = h.testService.CreateInstance(c.Request.Context(), user.ID, &input) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, gin.H{"message": "Instance created successfully"}) +} + +func (h *TestHandler) GetInstance(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "GetInstance", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + user, err := getUserFromContext(c) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + instance, err := h.testService.GetInstance(c.Request.Context(), user.ID, c.Param("scope"), c.Param("name")) + if err != nil { + logger.Debug(err.Error()) + // c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + // return + } + c.JSON(http.StatusOK, instance) +} + +func (h *TestHandler) GetInstanceStatus(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "GetInstanceStatus", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + user, err := getUserFromContext(c) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + status, err := h.testService.GetInstanceStatus(c.Request.Context(), user.ID, c.Param("scope"), c.Param("name")) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": status}) +} + +func (h *TestHandler) ExecuteInstance(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "ExecuteInstance", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + user, err := getUserFromContext(c) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + scope := c.Param("scope") + name := c.Param("name") + + output, err := h.testService.ExecuteInstance(c.Request.Context(), user.ID, scope, name) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"output": output}) +} diff --git a/internal/api/v1/handlers/test.go b/internal/api/v1/handlers/test.go new file mode 100644 index 0000000..4c23026 --- /dev/null +++ b/internal/api/v1/handlers/test.go @@ -0,0 +1,101 @@ +package handlers + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + + "github.com/celestiaorg/knuu/internal/api/v1/services" + "github.com/celestiaorg/knuu/internal/database/models" +) + +type TestHandler struct { + testService *services.TestService + logger *logrus.Logger +} + +func NewTestHandler(ts *services.TestService, logger *logrus.Logger) *TestHandler { + if logger == nil { + logger = logrus.New() + } + return &TestHandler{ + testService: ts, + logger: logger, + } +} + +func (h *TestHandler) CreateTest(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "CreateTest", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + user, err := getUserFromContext(c) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var input models.Test + if err := c.ShouldBindJSON(&input); err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + input.UserID = user.ID + if err := h.testService.Create(c.Request.Context(), &input); err != nil { + if errors.Is(err, services.ErrTestAlreadyExists) { + c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf("test already exists with scope: %s", input.Scope)}) + return + } + logger.Debug(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create test"}) + return + } + c.JSON(http.StatusCreated, gin.H{"message": "Test created successfully"}) +} + +func (h *TestHandler) GetTestDetails(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "GetTestDetails", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + _ = logger + var test models.Test + c.JSON(http.StatusOK, test) +} + +func (h *TestHandler) GetTestLogs(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "GetTestLogs", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + + user, err := getUserFromContext(c) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + scope := c.Param("scope") + + logFilePath, err := h.testService.TestLogsPath(c.Request.Context(), user.ID, scope) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get test logs"}) + return + } + + c.FileAttachment(logFilePath, fmt.Sprintf("%s.log", scope)) +} diff --git a/internal/api/v1/handlers/token.go b/internal/api/v1/handlers/token.go new file mode 100644 index 0000000..ee3dd57 --- /dev/null +++ b/internal/api/v1/handlers/token.go @@ -0,0 +1,5 @@ +package handlers + +// Users can request for new tokens with some permissions +// A user can have multiple tokens with different permissions +// A token can be revoked by the user or by the admin diff --git a/internal/api/v1/handlers/user.go b/internal/api/v1/handlers/user.go new file mode 100644 index 0000000..5afdd4d --- /dev/null +++ b/internal/api/v1/handlers/user.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "errors" + "net/http" + + "github.com/celestiaorg/knuu/internal/api/v1/middleware" + "github.com/celestiaorg/knuu/internal/api/v1/services" + "github.com/celestiaorg/knuu/internal/database/models" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +type UserHandler struct { + userService services.UserService + auth *middleware.Auth + logger *logrus.Logger +} + +func NewUserHandler(userService services.UserService, auth *middleware.Auth, logger *logrus.Logger) *UserHandler { + if logger == nil { + logger = logrus.New() + } + return &UserHandler{ + userService: userService, + auth: auth, + logger: logger, + } +} + +func (h *UserHandler) Register(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "Register", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + var input models.User + if err := c.ShouldBindJSON(&input); err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + _, err := h.userService.Register(c.Request.Context(), &input) + if err != nil { + logger.Debug(err.Error()) + if errors.Is(err, services.ErrUsernameAlreadyTaken) { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + return + } + c.JSON(http.StatusCreated, gin.H{"message": "User registered successfully"}) +} + +func (h *UserHandler) Login(c *gin.Context) { + logger := h.logger.WithFields(logrus.Fields{ + "handler": "Login", + "method": c.Request.Method, + "path": c.Request.URL.Path, + "clientIP": c.ClientIP(), + }) + var input struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + } + if err := c.ShouldBindJSON(&input); err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + user, err := h.userService.Authenticate(c.Request.Context(), input.Username, input.Password) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusUnauthorized, gin.H{"error": ErrInvalidCredentials.Error()}) + return + } + + token, err := h.auth.GenerateToken(user) + if err != nil { + logger.Debug(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"token": token}) +} diff --git a/internal/api/v1/handlers/utils.go b/internal/api/v1/handlers/utils.go new file mode 100644 index 0000000..9b5b381 --- /dev/null +++ b/internal/api/v1/handlers/utils.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "errors" + + "github.com/gin-gonic/gin" + + "github.com/celestiaorg/knuu/internal/api/v1/middleware" + "github.com/celestiaorg/knuu/internal/database/models" +) + +func getUserFromContext(c *gin.Context) (*models.User, error) { + user, ok := c.Get(middleware.UserContextKey) + if !ok { + return nil, errors.New("user not found in context") + } + authUser, ok := user.(*models.User) + if !ok { + return nil, errors.New("invalid user data in context") + } + return authUser, nil +} diff --git a/internal/api/v1/index.go b/internal/api/v1/index.go new file mode 100644 index 0000000..c42fd37 --- /dev/null +++ b/internal/api/v1/index.go @@ -0,0 +1,55 @@ +package api + +import ( + "fmt" + "net/http" + "runtime/debug" + "strings" + + "github.com/gin-gonic/gin" +) + +// IndexPage implements GET / +func (a *API) IndexPage(c *gin.Context) { + modName := "unknown" + buildInfo := "" + if bi, ok := debug.ReadBuildInfo(); ok { + modName = bi.Path + + buildInfo += "

Build Info:

" + for _, s := range bi.Settings { + buildInfo += fmt.Sprintf("", s.Key, s.Value) + } + buildInfo += "
%s%s
" + } + + html := `` + + html += fmt.Sprintf("Ciao, this is `%v` \n\n

", modName) + allAPIs := a.router.Routes() + html += "

List of endpoints:

" + for _, a := range allAPIs { + + href := strings.TrimPrefix(a.Path, "/") // it fixes the links if the service is running under a path + html += fmt.Sprintf(`%s [ %s ]
`, href, a.Path, a.Method) + } + html += buildInfo + + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(http.StatusOK, html) +} diff --git a/internal/api/v1/middleware/auth.go b/internal/api/v1/middleware/auth.go new file mode 100644 index 0000000..dd39ca8 --- /dev/null +++ b/internal/api/v1/middleware/auth.go @@ -0,0 +1,128 @@ +package middleware + +import ( + "errors" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" + + "github.com/celestiaorg/knuu/internal/database/models" +) + +const ( + UserTokenDuration = 24 * time.Hour + UserContextKey = "user" + + authTokenPrefix = "Bearer " + userTokenClaimsUserID = "user_id" + userTokenClaimsUsername = "username" + userTokenClaimsRole = "role" + userTokenClaimsExp = "exp" +) + +type Auth struct { + secretKey string +} + +func NewAuth(secretKey string) *Auth { + return &Auth{ + secretKey: secretKey, + } +} + +func (a *Auth) AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := a.getAuthToken(c) + if token == "" || !a.isValidToken(token) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + + user, err := a.getUserFromToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token err: " + err.Error()}) + c.Abort() + return + } + c.Set(UserContextKey, user) + c.Next() + } +} + +func (a *Auth) RequireRole(requiredRole models.UserRole) gin.HandlerFunc { + return func(c *gin.Context) { + user, err := a.getUserFromToken(a.getAuthToken(c)) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + c.Abort() + return + } + if user.Role != requiredRole { + c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) + c.Abort() + return + } + c.Next() + } +} + +func (a *Auth) GenerateToken(user *models.User) (string, error) { + claims := jwt.MapClaims{ + userTokenClaimsUserID: user.ID, + userTokenClaimsUsername: user.Username, + userTokenClaimsRole: user.Role, + userTokenClaimsExp: time.Now().Add(UserTokenDuration).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(a.secretKey)) +} + +func (a *Auth) getUserFromToken(token string) (*models.User, error) { + claims := jwt.MapClaims{} + _, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) { + return []byte(a.secretKey), nil + }) + if err != nil { + return nil, err + } + + userID, ok := claims[userTokenClaimsUserID].(float64) + if !ok { + return nil, errors.New("invalid user ID") + } + username, ok := claims[userTokenClaimsUsername].(string) + if !ok { + return nil, errors.New("invalid username") + } + role, ok := claims[userTokenClaimsRole].(float64) + if !ok { + return nil, errors.New("invalid role") + } + + return &models.User{ID: uint(userID), Username: username, Role: models.UserRole(role)}, nil +} + +func (a *Auth) isValidToken(token string) bool { + claims := &jwt.MapClaims{} + parsedToken, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) { + return []byte(a.secretKey), nil + }) + + if err != nil { + return false + } + + return parsedToken.Valid +} + +func (a *Auth) getAuthToken(c *gin.Context) string { + token := c.GetHeader("Authorization") + if len(token) > len(authTokenPrefix) && token[:len(authTokenPrefix)] == authTokenPrefix { + token = token[len(authTokenPrefix):] + } + return token +} diff --git a/internal/api/v1/paths.go b/internal/api/v1/paths.go new file mode 100644 index 0000000..8649194 --- /dev/null +++ b/internal/api/v1/paths.go @@ -0,0 +1,19 @@ +package api + +const ( + pathsPrefix = "/api/v1" + + pathsUser = pathsPrefix + "/user" + pathsUserRegister = pathsUser + "/register" + pathsUserLogin = pathsUser + "/login" + + pathsTests = pathsPrefix + "/tests" + pathsTestDetails = pathsTests + "/:scope" + pathsTestLogs = pathsTestDetails + "/logs" + pathsTestInstances = pathsTestDetails + "/instances" + pathsTestInstanceDetails = pathsTestInstances + "/:name" + pathsTestInstanceStatus = pathsTestInstanceDetails + "/status" + pathsTestInstanceLogs = pathsTestInstanceDetails + "/logs" + pathsTestInstanceStop = pathsTestInstanceDetails + "/stop" + pathsTestInstanceExecute = pathsTestInstanceDetails + "/execute" +) diff --git a/internal/api/v1/services/errors.go b/internal/api/v1/services/errors.go new file mode 100644 index 0000000..7649f72 --- /dev/null +++ b/internal/api/v1/services/errors.go @@ -0,0 +1,16 @@ +package services + +import "github.com/celestiaorg/knuu/pkg/errors" + +type Error = errors.Error + +var ( + ErrUsernameAlreadyTaken = errors.New("UsernameAlreadyTaken", "username already taken") + ErrUserNotFound = errors.New("UserNotFound", "user not found") + ErrCreatingAdminUser = errors.New("CreatingAdminUser", "error creating admin user") + ErrUserIDRequired = errors.New("UserIDRequired", "user ID is required") + ErrTestAlreadyExists = errors.New("TestAlreadyExists", "test already exists") + ErrTestNotFound = errors.New("TestNotFound", "test not found") + ErrScopeRequired = errors.New("ScopeRequired", "scope is required") + ErrLogFileNotFound = errors.New("LogFileNotFound", "log file not found") +) diff --git a/internal/api/v1/services/instance.go b/internal/api/v1/services/instance.go new file mode 100644 index 0000000..d121f35 --- /dev/null +++ b/internal/api/v1/services/instance.go @@ -0,0 +1,139 @@ +package services + +import ( + "context" + "fmt" + + "github.com/celestiaorg/knuu/pkg/builder" +) + +type Instance struct { + Name string `json:"name" binding:"required"` + Scope string `json:"scope"` + Image string `json:"image"` + GitContext builder.GitContext `json:"git_context"` + BuildArgs []string `json:"build_args"` + StartCommand []string `json:"start_command"` + Args []string `json:"args"` + StartNow bool `json:"start_now"` + Env map[string]string `json:"env"` + TCPPorts []int `json:"tcp_ports"` + UDPPorts []int `json:"udp_ports"` + Hostname string `json:"hostname"` // Readonly + + // Volumes []k8s.Volume `json:"volumes"` +} + +func (s *TestService) CreateInstance(ctx context.Context, userID uint, instance *Instance) error { + if userID == 0 { + return ErrUserIDRequired + } + + kn, err := s.Knuu(userID, instance.Scope) + if err != nil { + return err + } + + ins, err := kn.NewInstance(instance.Name) + if err != nil { + return err + } + + buildArgs := []builder.ArgInterface{} + for _, arg := range instance.BuildArgs { + buildArgs = append(buildArgs, &builder.BuildArg{Value: arg}) + } + + if instance.Image != "" { + if err := ins.Build().SetImage(ctx, instance.Image, buildArgs...); err != nil { + return err + } + } + + if len(instance.StartCommand) > 0 { + if err := ins.Build().SetStartCommand(instance.StartCommand...); err != nil { + return err + } + } + + if len(instance.Args) > 0 { + if err := ins.Build().SetArgs(instance.Args...); err != nil { + return err + } + } + + for k, v := range instance.Env { + if err := ins.Build().SetEnvironmentVariable(k, v); err != nil { + return err + } + } + + if instance.GitContext.Repo != "" { + if err := ins.Build().SetGitRepo(ctx, instance.GitContext, buildArgs...); err != nil { + return err + } + } + + for _, port := range instance.TCPPorts { + if err := ins.Network().AddPortTCP(port); err != nil { + return err + } + } + + for _, port := range instance.UDPPorts { + if err := ins.Network().AddPortUDP(port); err != nil { + return err + } + } + + if !instance.StartNow { + return nil + } + + if err := ins.Build().Commit(ctx); err != nil { + return err + } + return ins.Execution().StartAsync(ctx) +} + +func (s *TestService) GetInstance(ctx context.Context, userID uint, scope, instanceName string) (*Instance, error) { + kn, err := s.Knuu(userID, scope) + if err != nil { + return nil, err + } + + _ = kn + + var instance Instance + instance.Name = instanceName + instance.Scope = scope + + return &instance, nil +} + +func (s *TestService) GetInstanceStatus(ctx context.Context, userID uint, scope, instanceName string) (string, error) { + kn, err := s.Knuu(userID, scope) + if err != nil { + return "", err + } + + ps, err := kn.K8sClient.PodStatus(ctx, instanceName) + if err != nil { + return "", err + } + + return string(ps.Status), nil +} + +func (s *TestService) ExecuteInstance(ctx context.Context, userID uint, scope, instanceName string) (string, error) { + kn, err := s.Knuu(userID, scope) + if err != nil { + return "", err + } + + _ = kn + // TODO: we need to implement something in knuu where we can access the instance while it is being running in k8s + // and knuu object itself is created afterwards something like search it by name and get the instance onject + + return "", fmt.Errorf("not implemented") +} diff --git a/internal/api/v1/services/test.go b/internal/api/v1/services/test.go new file mode 100644 index 0000000..6db4762 --- /dev/null +++ b/internal/api/v1/services/test.go @@ -0,0 +1,390 @@ +package services + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/sirupsen/logrus" + apierrs "k8s.io/apimachinery/pkg/api/errors" + + "github.com/celestiaorg/knuu/internal/database" + "github.com/celestiaorg/knuu/internal/database/models" + "github.com/celestiaorg/knuu/internal/database/repos" + "github.com/celestiaorg/knuu/pkg/k8s" + "github.com/celestiaorg/knuu/pkg/knuu" + "github.com/celestiaorg/knuu/pkg/minio" +) + +const ( + DefaultTestTimeout = time.Hour * 1 + DefaultNamespace = "default" + DefaultTestLogsPath = "/tmp/knuu-logs" // directory to store logs of each test + LogsDirPermission = 0755 + LogsFilePermission = 0644 + PeriodicCleanupInterval = time.Minute * 10 +) + +type testServiceCleanup struct { + logFiles []*os.File +} + +type TestService struct { + repo *repos.TestRepository + knuuList map[uint]map[string]*knuu.Knuu // key is the user ID, second key is the scope + knuuListMu sync.RWMutex + defaultK8sClient *k8s.Client + testsLogsPath string + cleanup *testServiceCleanup + logger *logrus.Logger + stopCleanupChan chan struct{} +} + +type TestServiceOptions struct { + TestsLogsPath string // optional directory where the logs of all tests will be stored each test has one log file + Logger *logrus.Logger +} + +func NewTestService(ctx context.Context, repo *repos.TestRepository, opts TestServiceOptions) (*TestService, error) { + opts = setServiceOptsDefaults(opts) + + s := &TestService{ + repo: repo, + knuuList: make(map[uint]map[string]*knuu.Knuu), + testsLogsPath: opts.TestsLogsPath, + logger: opts.Logger, + stopCleanupChan: make(chan struct{}), + } + + if _, err := os.Stat(s.testsLogsPath); os.IsNotExist(err) { + if err := os.MkdirAll(s.testsLogsPath, LogsDirPermission); err != nil { + return nil, err + } + } + + k8sClient, err := k8s.NewClient(ctx, DefaultNamespace, logrus.New()) + if err != nil { + return nil, err + } + s.defaultK8sClient = k8sClient + + if err := s.loadRunningTestsFromDB(ctx); err != nil { + return nil, err + } + + go s.startPeriodicCleanup() + return s, nil +} + +func setServiceOptsDefaults(opts TestServiceOptions) TestServiceOptions { + if opts.Logger == nil { + opts.Logger = logrus.New() + } + + if opts.TestsLogsPath == "" { + opts.TestsLogsPath = DefaultTestLogsPath + } + + return opts +} + +func (s *TestService) Create(ctx context.Context, test *models.Test) error { + if test.UserID == 0 { + return ErrUserIDRequired + } + + err := s.repo.Create(ctx, test) + if database.IsDuplicateKeyError(err) { + return ErrTestAlreadyExists + } else if err != nil { + return err + } + + // TODO: currently this process is blocking the request until the knuu is ready + // we need to make it non-blocking + err = s.prepareKnuu(ctx, test) + if err == nil { + return nil + } + + return errors.Join(err, s.repo.Delete(ctx, test.Scope)) +} + +func (s *TestService) Knuu(userID uint, scope string) (*knuu.Knuu, error) { + s.knuuListMu.RLock() + defer s.knuuListMu.RUnlock() + + kn, ok := s.knuuList[userID][scope] + if !ok { + return nil, ErrTestNotFound + } + + return kn, nil +} + +func (s *TestService) Delete(ctx context.Context, userID uint, scope string) error { + if err := s.forceCleanupTest(ctx, userID, scope); err != nil { + return err + } + + return s.repo.Delete(ctx, scope) +} + +func (s *TestService) Details(ctx context.Context, userID uint, scope string) (*models.Test, error) { + return s.repo.Get(ctx, userID, scope) +} + +func (s *TestService) List(ctx context.Context, userID uint, limit int, offset int) ([]models.Test, error) { + return s.repo.List(ctx, userID, limit, offset) +} + +func (s *TestService) Count(ctx context.Context, userID uint) (int64, error) { + return s.repo.Count(ctx, userID) +} + +func (s *TestService) Update(ctx context.Context, userID uint, scope string, test *models.Test) error { + // for security reasons, these have to be explicitly set + test.UserID = userID + test.Scope = scope + return s.repo.Update(ctx, test) +} + +func (s *TestService) SetFinished(ctx context.Context, userID uint, scope string) error { + test, err := s.repo.Get(ctx, userID, scope) + if err != nil { + return err + } + + test.Finished = true + return s.repo.Update(ctx, test) +} + +func (s *TestService) Shutdown(ctx context.Context) error { + close(s.stopCleanupChan) + for userID, users := range s.knuuList { + for scope := range users { + if err := s.cleanupIfFinishedTest(ctx, userID, scope); err != nil { + return err + } + } + } + + if s.cleanup == nil { + return nil + } + + for _, logFile := range s.cleanup.logFiles { + if logFile == nil { + continue + } + + if err := logFile.Close(); err != nil { + return err + } + } + s.cleanup.logFiles = nil + + return nil +} + +func (s *TestService) TestLogsPath(ctx context.Context, userID uint, scope string) (string, error) { + // TODO: we need to apply roles here so Admins can access all tests logs + // Check if the test exists and beongs to the user + _, err := s.repo.Get(ctx, userID, scope) + if err != nil { + return "", err + } + + logFilePath := filepath.Join(s.testsLogsPath, fmt.Sprintf("%s.log", scope)) + + if _, err := os.Stat(logFilePath); os.IsNotExist(err) { + return "", ErrLogFileNotFound + } else if err != nil { + return "", err + } + + return logFilePath, nil +} + +func (s *TestService) cleanupIfFinishedTest(ctx context.Context, userID uint, scope string) error { + running, err := s.isTestRunning(ctx, scope) + if err != nil { + return err + } + if !running { + return nil + } + + return s.forceCleanupTest(ctx, userID, scope) +} + +func (s *TestService) forceCleanupTest(ctx context.Context, userID uint, scope string) error { + if err := s.SetFinished(ctx, userID, scope); err != nil { + return err + } + + kn, ok := s.knuuList[userID][scope] + if !ok { + return nil + } + + if err := kn.CleanUp(ctx); err != nil { + return err + } + + s.knuuListMu.Lock() + defer s.knuuListMu.Unlock() + + delete(s.knuuList[userID], scope) + if len(s.knuuList[userID]) == 0 { + delete(s.knuuList, userID) + } + return nil +} + +func (s *TestService) isTestRunning(ctx context.Context, scope string) (bool, error) { + _, err := s.defaultK8sClient.GetNamespace(ctx, scope) + if apierrs.IsNotFound(err) { + return false, nil + } + return err == nil, err +} + +func (s *TestService) loadRunningTestsFromDB(ctx context.Context) error { + tests, err := s.repo.ListAllAlive(ctx) + if err != nil { + return err + } + + for _, test := range tests { + isRunning, err := s.isTestRunning(ctx, test.Scope) + if err != nil { + return err + } + if !isRunning { + continue + } + + err = s.prepareKnuu(ctx, &test) + if err != nil && err != ErrTestAlreadyExists { + return err + } + } + + return nil +} + +func (s *TestService) prepareKnuu(ctx context.Context, test *models.Test) error { + if err := k8s.ValidateNamespace(test.Scope); err != nil { + return err + } + + s.knuuListMu.Lock() + if _, ok := s.knuuList[test.UserID]; !ok { + s.knuuList[test.UserID] = make(map[string]*knuu.Knuu) + } + s.knuuListMu.Unlock() + + if test.Scope == "" { + return ErrScopeRequired + } + + _, ok := s.knuuList[test.UserID][test.Scope] + if ok { + return ErrTestAlreadyExists + } + + logFile, err := os.OpenFile( + filepath.Join(s.testsLogsPath, fmt.Sprintf("%s.log", test.Scope)), + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + LogsFilePermission, + ) + if err != nil { + s.logger.Errorf("opening log file for test %s: %v", test.Scope, err) + return err + } + s.cleanup.logFiles = append(s.cleanup.logFiles, logFile) + + testLogger := logrus.New() + testLogger.SetOutput(logFile) + + if test.LogLevel != "" { + level, err := logrus.ParseLevel(test.LogLevel) + if err != nil { + return err + } + testLogger.SetLevel(level) + } + + k8sClient, err := k8s.NewClient(ctx, test.Scope, testLogger) + if err != nil { + return err + } + + var minioClient *minio.Minio + if test.MinioEnabled { + minioClient, err = minio.New(ctx, k8sClient, testLogger) + if err != nil { + return err + } + } + + if test.Deadline.IsZero() { + test.Deadline = time.Now().Add(DefaultTestTimeout) + } + + kn, err := knuu.New(ctx, knuu.Options{ + ProxyEnabled: test.ProxyEnabled, + K8sClient: k8sClient, + MinioClient: minioClient, + Timeout: time.Until(test.Deadline), // TODO: replace it with deadline when the deadline PR is merged + }) + if err != nil { + return err + } + + s.knuuListMu.Lock() + defer s.knuuListMu.Unlock() + s.knuuList[test.UserID][test.Scope] = kn + + return nil +} + +func (s *TestService) startPeriodicCleanup() { + ticker := time.NewTicker(PeriodicCleanupInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + s.performCleanup() + case <-s.stopCleanupChan: + s.logger.Info("TestService: Stopping periodic cleanup") + return + } + } +} + +func (s *TestService) performCleanup() { + s.knuuListMu.RLock() + userScopes := make(map[uint][]string) + for userID, users := range s.knuuList { + for scope := range users { + userScopes[userID] = append(userScopes[userID], scope) + } + } + s.knuuListMu.RUnlock() + + for userID, scopes := range userScopes { + for _, scope := range scopes { + s.logger.Debugf("TestService: Running periodic cleanup for userID: %d, scope: %s", userID, scope) + if err := s.cleanupIfFinishedTest(context.Background(), userID, scope); err != nil { + s.logger.Errorf("TestService: Error cleaning up test %s for user %d: %v", scope, userID, err) + } + } + } +} diff --git a/internal/api/v1/services/user.go b/internal/api/v1/services/user.go new file mode 100644 index 0000000..d101ea9 --- /dev/null +++ b/internal/api/v1/services/user.go @@ -0,0 +1,72 @@ +package services + +import ( + "context" + + "golang.org/x/crypto/bcrypt" + + "github.com/celestiaorg/knuu/internal/database/models" + "github.com/celestiaorg/knuu/internal/database/repos" +) + +type UserService interface { + Register(ctx context.Context, user *models.User) (*models.User, error) + Authenticate(ctx context.Context, username, password string) (*models.User, error) +} + +type userServiceImpl struct { + repo repos.UserRepository +} + +var _ UserService = &userServiceImpl{} + +// This function is used to create the admin user and the user service. +// It is called when the API is initialized. +func NewUserService(ctx context.Context, adminUser, adminPass string, userRepo repos.UserRepository) (UserService, error) { + us := &userServiceImpl{ + repo: userRepo, + } + + _, err := us.Register(ctx, + &models.User{ + Username: adminUser, + Password: adminPass, + Role: models.RoleAdmin, + }) + if err != nil && err != ErrUsernameAlreadyTaken { + return nil, ErrCreatingAdminUser.Wrap(err) + } + + return us, nil +} + +func (s *userServiceImpl) Register(ctx context.Context, user *models.User) (*models.User, error) { + if _, err := s.repo.FindUserByUsername(ctx, user.Username); err == nil { + return nil, ErrUsernameAlreadyTaken + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + user.Password = string(hashedPassword) + if err := s.repo.CreateUser(ctx, user); err != nil { + return nil, err + } + + return user, nil +} + +func (s *userServiceImpl) Authenticate(ctx context.Context, username, password string) (*models.User, error) { + user, err := s.repo.FindUserByUsername(ctx, username) + if err != nil { + return nil, err + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return nil, err + } + + return user, nil +} diff --git a/internal/database/db.go b/internal/database/db.go new file mode 100644 index 0000000..a946e99 --- /dev/null +++ b/internal/database/db.go @@ -0,0 +1,92 @@ +package database + +import ( + "errors" + "fmt" + + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "github.com/celestiaorg/knuu/internal/database/models" +) + +const ( + DefaultHost = "localhost" + DefaultUser = "postgres" + DefaultPassword = "postgres" + DefaultDBName = "postgres" + DefaultPort = 5432 + DefaultSSLEnabled = false +) + +type Options struct { + Host string + User string + Password string + DBName string + Port int + SSLEnabled *bool + LogLevel logger.LogLevel +} + +func New(opts Options) (*gorm.DB, error) { + opts = setDefaults(opts) + sslMode := "disable" + if opts.SSLEnabled != nil && *opts.SSLEnabled { + sslMode = "enable" + } + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s", + opts.Host, opts.User, opts.Password, opts.DBName, opts.Port, sslMode) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, err + } + if err := migrate(db); err != nil { + return nil, err + } + + db.Logger = db.Logger.LogMode(opts.LogLevel) + return db, nil +} + +// Please note that this function works only with postgres. +// For other databases, you need to implement your own function. +func IsDuplicateKeyError(err error) bool { + return errors.Is(postgres.Dialector{}.Translate(err), gorm.ErrDuplicatedKey) +} + +func setDefaults(opts Options) Options { + if opts.Host == "" { + opts.Host = DefaultHost + } + if opts.User == "" { + opts.User = DefaultUser + } + if opts.Password == "" { + opts.Password = DefaultPassword + } + if opts.DBName == "" { + opts.DBName = DefaultDBName + } + if opts.Port == 0 { + opts.Port = DefaultPort + } + if opts.SSLEnabled == nil { + sslMode := DefaultSSLEnabled + opts.SSLEnabled = &sslMode + } + if opts.LogLevel == 0 { + opts.LogLevel = logger.Warn + } + return opts +} + +func migrate(db *gorm.DB) error { + return db.AutoMigrate( + &models.User{}, + &models.Token{}, + &models.Permission{}, + &models.Test{}, + ) +} diff --git a/internal/database/models/test.go b/internal/database/models/test.go new file mode 100644 index 0000000..5ce9283 --- /dev/null +++ b/internal/database/models/test.go @@ -0,0 +1,23 @@ +package models + +import ( + "time" +) + +const ( + TestFinishedField = "finished" + TestCreatedAtField = "created_at" +) + +type Test struct { + Scope string `json:"scope" gorm:"primaryKey; varchar(255)"` + UserID uint `json:"-" gorm:"index"` // the owner of the test + Title string `json:"title" gorm:""` + MinioEnabled bool `json:"minio_enabled" gorm:""` + ProxyEnabled bool `json:"proxy_enabled" gorm:""` + Deadline time.Time `json:"deadline" gorm:"index"` + CreatedAt time.Time `json:"created_at" gorm:"index"` + UpdatedAt time.Time `json:"updated_at"` + Finished bool `json:"finished" gorm:"index"` + LogLevel string `json:"log_level" gorm:""` // logrus level as string (e.g. "debug", "info", "warn", "error", "fatal", "panic") +} diff --git a/internal/database/models/token.go b/internal/database/models/token.go new file mode 100644 index 0000000..2640e7f --- /dev/null +++ b/internal/database/models/token.go @@ -0,0 +1 @@ +package models diff --git a/internal/database/models/user.go b/internal/database/models/user.go new file mode 100644 index 0000000..9a98daa --- /dev/null +++ b/internal/database/models/user.go @@ -0,0 +1,58 @@ +package models + +import ( + "time" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type UserRole int + +const ( + RoleUser UserRole = iota + RoleAdmin +) + +type User struct { + ID uint `json:"-" gorm:"primaryKey"` + Username string `json:"username" gorm:"unique;not null"` + Password string `json:"password" gorm:"not null"` + Role UserRole `json:"role" gorm:"not null"` + CreatedAt time.Time `json:"created_at"` +} + +type Token struct { + ID uint `json:"-" gorm:"primaryKey"` + UserID uint `json:"-" gorm:"index;not null"` + Token string `json:"token" gorm:"unique;not null"` + ExpiresAt time.Time `json:"expires_at" gorm:"not null"` +} + +type AccessLevel int + +const ( + AccessLevelRead AccessLevel = iota + 1 + AccessLevelWrite + AccessLevelAdmin +) + +type Permission struct { + ID uint `json:"-" gorm:"primaryKey"` + UserID uint `json:"-" gorm:"index;not null"` + Resource string `json:"resource" gorm:"not null"` + AccessLevel AccessLevel `json:"access_level" gorm:"not null"` +} + +func (u *User) BeforeCreate(tx *gorm.DB) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + u.Password = string(hashedPassword) + return nil +} + +func (u *User) ValidatePassword(password string) bool { + return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) == nil +} diff --git a/internal/database/repos/test.go b/internal/database/repos/test.go new file mode 100644 index 0000000..760c53a --- /dev/null +++ b/internal/database/repos/test.go @@ -0,0 +1,60 @@ +package repos + +import ( + "context" + + "gorm.io/gorm" + + "github.com/celestiaorg/knuu/internal/database/models" +) + +type TestRepository struct { + db *gorm.DB +} + +func NewTestRepository(db *gorm.DB) *TestRepository { + return &TestRepository{ + db: db, + } +} + +func (r *TestRepository) Create(ctx context.Context, test *models.Test) error { + return r.db.WithContext(ctx).Create(test).Error +} + +func (r *TestRepository) Get(ctx context.Context, userID uint, scope string) (*models.Test, error) { + var test models.Test + err := r.db.WithContext(ctx).Where(&models.Test{UserID: userID, Scope: scope}).First(&test).Error + return &test, err +} + +func (r *TestRepository) Delete(ctx context.Context, scope string) error { + return r.db.WithContext(ctx).Delete(&models.Test{Scope: scope}).Error +} + +func (r *TestRepository) Update(ctx context.Context, test *models.Test) error { + return r.db.WithContext(ctx).Model(&models.Test{}).Where(&models.Test{Scope: test.Scope, UserID: test.UserID}).Updates(test).Error +} + +func (r *TestRepository) List(ctx context.Context, userID uint, limit int, offset int) ([]models.Test, error) { + var tests []models.Test + err := r.db.WithContext(ctx). + Where(&models.Test{UserID: userID}). + Limit(limit).Offset(offset). + Order(models.TestFinishedField + " ASC"). + Order(models.TestCreatedAtField + " DESC"). + Find(&tests).Error + return tests, err +} + +func (r *TestRepository) Count(ctx context.Context, userID uint) (int64, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&models.Test{}).Where(&models.Test{UserID: userID}).Count(&count).Error + return count, err +} + +func (r *TestRepository) ListAllAlive(ctx context.Context) ([]models.Test, error) { + var tests []models.Test + err := r.db.WithContext(ctx).Where(&models.Test{Finished: false}).Find(&tests).Error + return tests, err +} diff --git a/internal/database/repos/token.go b/internal/database/repos/token.go new file mode 100644 index 0000000..2b0d3a9 --- /dev/null +++ b/internal/database/repos/token.go @@ -0,0 +1 @@ +package repos diff --git a/internal/database/repos/user.go b/internal/database/repos/user.go new file mode 100644 index 0000000..30514c8 --- /dev/null +++ b/internal/database/repos/user.go @@ -0,0 +1,53 @@ +package repos + +import ( + "context" + + "github.com/celestiaorg/knuu/internal/database/models" + + "gorm.io/gorm" +) + +type UserRepository interface { + CreateUser(ctx context.Context, user *models.User) error + FindUserByUsername(ctx context.Context, username string) (*models.User, error) + FindUserByID(ctx context.Context, id uint) (*models.User, error) + UpdatePassword(ctx context.Context, id uint, password string) error + DeleteUserById(ctx context.Context, id uint) error +} + +type userRepositoryImpl struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepositoryImpl{db: db} +} + +func (r *userRepositoryImpl) CreateUser(ctx context.Context, user *models.User) error { + return r.db.WithContext(ctx).Create(user).Error +} + +func (r *userRepositoryImpl) FindUserByUsername(ctx context.Context, username string) (*models.User, error) { + var user models.User + err := r.db.WithContext(ctx).Where(&models.User{Username: username}).First(&user).Error + return &user, err +} + +func (r *userRepositoryImpl) FindUserByID(ctx context.Context, id uint) (*models.User, error) { + var user models.User + err := r.db.WithContext(ctx).Where(&models.User{ID: id}).First(&user).Error + return &user, err +} + +func (r *userRepositoryImpl) UpdatePassword(ctx context.Context, id uint, password string) error { + updatedUser := &models.User{ + Password: password, + } + return r.db.WithContext(ctx).Model(&models.User{}). + Where(&models.User{ID: id}).Updates(updatedUser).Error +} + +func (r *userRepositoryImpl) DeleteUserById(ctx context.Context, id uint) error { + return r.db.WithContext(ctx).Delete(&models.User{ID: id}).Error +} diff --git a/pkg/builder/git.go b/pkg/builder/git.go index a6418e7..3b313dd 100644 --- a/pkg/builder/git.go +++ b/pkg/builder/git.go @@ -12,11 +12,11 @@ const ( ) type GitContext struct { - Repo string - Branch string - Commit string - Username string - Password string + Repo string `json:"repo"` + Branch string `json:"branch"` + Commit string `json:"commit"` + Username string `json:"username"` + Password string `json:"password"` } // This build context follows Kaniko build context pattern diff --git a/pkg/k8s/namespace.go b/pkg/k8s/namespace.go index 308922e..5a233c5 100644 --- a/pkg/k8s/namespace.go +++ b/pkg/k8s/namespace.go @@ -12,7 +12,7 @@ func (c *Client) CreateNamespace(ctx context.Context, name string) error { if c.terminated { return ErrClientTerminated } - if err := validateNamespace(name); err != nil { + if err := ValidateNamespace(name); err != nil { return err } @@ -37,7 +37,7 @@ func (c *Client) CreateNamespace(ctx context.Context, name string) error { func (c *Client) DeleteNamespace(ctx context.Context, name string) error { err := c.clientset.CoreV1().Namespaces().Delete(ctx, name, metav1.DeleteOptions{}) - if err != nil { + if err != nil && !apierrs.IsNotFound(err) { return ErrDeletingNamespace.WithParams(name).Wrap(err) } return nil diff --git a/pkg/k8s/namespace_test.go b/pkg/k8s/namespace_test.go index 9f10013..cfff6b5 100644 --- a/pkg/k8s/namespace_test.go +++ b/pkg/k8s/namespace_test.go @@ -2,7 +2,6 @@ package k8s_test import ( "context" - "errors" corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" @@ -88,11 +87,10 @@ func (s *TestSuite) TestDeleteNamespace() { expectedErr: nil, }, { - name: "namespace not found", - namespace: "non-existent-namespace", - setupMock: func() {}, - expectedErr: k8s.ErrDeletingNamespace.WithParams("non-existent-namespace"). - Wrap(errors.New("namespaces \"non-existent-namespace\" not found")), + name: "namespace not found", + namespace: "non-existent-namespace", + setupMock: func() {}, + expectedErr: nil, }, { name: "client error", diff --git a/pkg/k8s/pod.go b/pkg/k8s/pod.go index 035923a..d6f61ce 100644 --- a/pkg/k8s/pod.go +++ b/pkg/k8s/pod.go @@ -66,9 +66,9 @@ type PodConfig struct { } type Volume struct { - Path string - Size resource.Quantity - Owner int64 + Path string `json:"path"` + Size resource.Quantity `json:"size"` + Owner int64 `json:"owner"` } type File struct { diff --git a/pkg/k8s/validate.go b/pkg/k8s/validate.go index 3b654bd..09c06f1 100644 --- a/pkg/k8s/validate.go +++ b/pkg/k8s/validate.go @@ -24,7 +24,7 @@ func validateDNS1123Subdomain(name string, returnErr *errors.Error) error { return nil } -func validateNamespace(name string) error { +func ValidateNamespace(name string) error { return validateDNS1123Label(name, ErrInvalidNamespaceName) } @@ -169,7 +169,7 @@ func validatePodConfig(podConfig PodConfig) error { return err } - if err := validateNamespace(podConfig.Namespace); err != nil { + if err := ValidateNamespace(podConfig.Namespace); err != nil { return err } @@ -212,7 +212,7 @@ func validateReplicaSetConfig(rsConfig ReplicaSetConfig) error { if err := validateReplicaSetName(rsConfig.Name); err != nil { return err } - if err := validateNamespace(rsConfig.Namespace); err != nil { + if err := ValidateNamespace(rsConfig.Namespace); err != nil { return err } if err := validateLabels(rsConfig.Labels); err != nil { diff --git a/pkg/k8s/validate_test.go b/pkg/k8s/validate_test.go index a7fe2b7..b370627 100644 --- a/pkg/k8s/validate_test.go +++ b/pkg/k8s/validate_test.go @@ -57,7 +57,7 @@ func TestValidateNamespace(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := validateNamespace(test.input) + err := ValidateNamespace(test.input) assert.Equal(t, test.expected, err) }) }