From 7f5ddc8eaab80ec0dba00b5498426c4f7372fff0 Mon Sep 17 00:00:00 2001 From: mojtaba-esk Date: Wed, 15 Jan 2025 09:38:39 +0330 Subject: [PATCH 1/8] first draft commit --- .gitignore | 3 + Makefile | 10 + cmd/api/api.go | 148 +++++++++++++++ cmd/main.go | 12 ++ cmd/root/root.go | 22 +++ go.mod | 61 +++--- go.sum | 186 +++++++------------ internal/api/v1/api.go | 105 +++++++++++ internal/api/v1/handlers/test_handler.go | 1 + internal/api/v1/handlers/token_handler.go | 1 + internal/api/v1/handlers/user.go | 51 +++++ internal/api/v1/index.go | 55 ++++++ internal/api/v1/middleware/auth.go | 24 +++ internal/api/v1/middleware/roles.go | 19 ++ internal/api/v1/paths.go | 9 + internal/api/v1/services/auth_service.go | 1 + internal/api/v1/services/resource_service.go | 1 + internal/api/v1/services/user.go | 76 ++++++++ internal/database/db.go | 66 +++++++ internal/database/models/test.go | 1 + internal/database/models/token.go | 1 + internal/database/models/user.go | 40 ++++ internal/database/repos/test.go | 1 + internal/database/repos/token.go | 1 + internal/database/repos/user.go | 51 +++++ 25 files changed, 798 insertions(+), 148 deletions(-) create mode 100644 cmd/api/api.go create mode 100644 cmd/main.go create mode 100644 cmd/root/root.go create mode 100644 internal/api/v1/api.go create mode 100644 internal/api/v1/handlers/test_handler.go create mode 100644 internal/api/v1/handlers/token_handler.go create mode 100644 internal/api/v1/handlers/user.go create mode 100644 internal/api/v1/index.go create mode 100644 internal/api/v1/middleware/auth.go create mode 100644 internal/api/v1/middleware/roles.go create mode 100644 internal/api/v1/paths.go create mode 100644 internal/api/v1/services/auth_service.go create mode 100644 internal/api/v1/services/resource_service.go create mode 100644 internal/api/v1/services/user.go create mode 100644 internal/database/db.go create mode 100644 internal/database/models/test.go create mode 100644 internal/database/models/token.go create mode 100644 internal/database/models/user.go create mode 100644 internal/database/repos/test.go create mode 100644 internal/database/repos/token.go create mode 100644 internal/database/repos/user.go diff --git a/.gitignore b/.gitignore index db77c52..93fcc47 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ coverage.txt .devcontainer Taskfile.yaml +bin/* +docker-compose.yml +.env diff --git a/Makefile b/Makefile index 284ff30..41b381e 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 test ## 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..8f21656 --- /dev/null +++ b/cmd/api/api.go @@ -0,0 +1,148 @@ +package api + +import ( + "fmt" + + "github.com/celestiaorg/knuu/internal/api/v1" + "github.com/celestiaorg/knuu/internal/database" + + "github.com/gin-gonic/gin" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const ( + apiCmdName = "api" + + flagPort = "port" + flagLogLevel = "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" + + defaultPort = 8080 + defaultLogLevel = gin.ReleaseMode + + defaultDBHost = "localhost" + defaultDBUser = "postgres" + defaultDBPassword = "postgres" + defaultDBName = "postgres" + defaultDBPort = 5432 + + defaultSecretKey = "secret" + defaultAdminUser = "admin" + defaultAdminPass = "admin" +) + +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(flagLogLevel, "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") + + return apiCmd +} + +func runAPIServer(cmd *cobra.Command, args []string) error { + port, err := cmd.Flags().GetInt(flagPort) + if err != nil { + return fmt.Errorf("failed to get port: %v", err) + } + + logLevel, err := cmd.Flags().GetString(flagLogLevel) + if err != nil { + return fmt.Errorf("failed to get log level: %v", err) + } + + 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) + } + + secretKey, err := cmd.Flags().GetString(flagSecretKey) + if err != nil { + return fmt.Errorf("failed to get secret key: %v", err) + } + + adminUser, err := cmd.Flags().GetString(flagAdminUser) + if err != nil { + return fmt.Errorf("failed to get admin user: %v", err) + } + + adminPass, err := cmd.Flags().GetString(flagAdminPass) + if err != nil { + return fmt.Errorf("failed to get admin password: %v", err) + } + + apiServer := api.New(db, api.Options{ + Port: port, + LogMode: logLevel, + SecretKey: secretKey, + AdminUser: adminUser, + AdminPass: adminPass, + }) + + 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) + } + + return database.Options{ + Host: dbHost, + User: dbUser, + Password: dbPassword, + DBName: dbName, + Port: dbPort, + }, nil +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..b7ce832 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/celestiaorg/knuu/cmd/root" + "github.com/sirupsen/logrus" +) + +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..b0c168e 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,18 @@ 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/v4 v4.5.1 github.com/google/uuid v1.6.0 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/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 +23,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 +54,38 @@ 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 +94,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..93ece83 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/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 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,89 @@ 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/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 +227,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 +238,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 +249,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 +275,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 +287,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 +301,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 +321,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 +339,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..8d7c82a --- /dev/null +++ b/internal/api/v1/api.go @@ -0,0 +1,105 @@ +package api + +import ( + "fmt" + "net/http" + + "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/repos" + + "github.com/gin-gonic/gin" + "github.com/rs/cors" + "gorm.io/gorm" +) + +const ( + defaultPort = 8080 + defaultLogMode = gin.ReleaseMode +) + +type API struct { + router *gin.Engine + server *http.Server +} + +type Options struct { + Port int + LogMode string // gin.DebugMode, gin.ReleaseMode(default), gin.TestMode + OriginAllowed string + SecretKey string +} + +func New(db *gorm.DB, opts Options) *API { + opts = setDefaults(opts) + gin.SetMode(opts.LogMode) + + rt := gin.Default() + + public := rt.Group("/") + { + uh := handlers.NewUserHandler(services.NewUserService(opts.SecretKey, repos.NewUserRepository(db))) + public.POST(pathsUserRegister, uh.Register) + public.POST(pathsUserLogin, uh.Login) + } + protected := rt.Group("/", middleware.AuthMiddleware()) + + _ = protected + + a := &API{ + router: rt, + server: &http.Server{ + Addr: fmt.Sprintf(":%d", opts.Port), + Handler: handleOrigin(rt, opts.OriginAllowed), + }, + } + + if opts.LogMode != gin.ReleaseMode { + public.GET("/", a.IndexPage) + } + + return a +} + +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() error { + fmt.Println("Shutting down API server") + return a.server.Close() +} + +func setDefaults(opts Options) Options { + if opts.Port == 0 { + opts.Port = defaultPort + } + + if opts.LogMode == "" { + opts.LogMode = defaultLogMode + } + + if opts.SecretKey == "" { + opts.SecretKey = "secret" + } + + 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) +} diff --git a/internal/api/v1/handlers/test_handler.go b/internal/api/v1/handlers/test_handler.go new file mode 100644 index 0000000..5ac8282 --- /dev/null +++ b/internal/api/v1/handlers/test_handler.go @@ -0,0 +1 @@ +package handlers diff --git a/internal/api/v1/handlers/token_handler.go b/internal/api/v1/handlers/token_handler.go new file mode 100644 index 0000000..5ac8282 --- /dev/null +++ b/internal/api/v1/handlers/token_handler.go @@ -0,0 +1 @@ +package handlers diff --git a/internal/api/v1/handlers/user.go b/internal/api/v1/handlers/user.go new file mode 100644 index 0000000..35ea342 --- /dev/null +++ b/internal/api/v1/handlers/user.go @@ -0,0 +1,51 @@ +package handlers + +import ( + "net/http" + + "github.com/celestiaorg/knuu/internal/api/v1/services" + "github.com/celestiaorg/knuu/internal/database/models" + + "github.com/gin-gonic/gin" +) + +type UserHandler struct { + userService services.UserService +} + +func NewUserHandler(userService services.UserService) *UserHandler { + return &UserHandler{userService: userService} +} + +func (h *UserHandler) Register(c *gin.Context) { + var input models.User + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + user, err := h.userService.Register(&input) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, user) +} + +func (h *UserHandler) Login(c *gin.Context) { + var input struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + } + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + token, err := h.userService.Authenticate(input.Username, input.Password) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"token": token}) +} diff --git a/internal/api/v1/index.go b/internal/api/v1/index.go new file mode 100644 index 0000000..6c8fad9 --- /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
`, href, a.Path) + } + 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..65d69b8 --- /dev/null +++ b/internal/api/v1/middleware/auth.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" || !isValidToken(token) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + c.Next() + } +} + +func isValidToken(token string) bool { + // Implement token validation logic + return token == "valid-token" +} diff --git a/internal/api/v1/middleware/roles.go b/internal/api/v1/middleware/roles.go new file mode 100644 index 0000000..670bd55 --- /dev/null +++ b/internal/api/v1/middleware/roles.go @@ -0,0 +1,19 @@ +package middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func RequireRole(requiredRole string) gin.HandlerFunc { + return func(c *gin.Context) { + role := c.GetString("role") // Set during JWT parsing + if role != requiredRole { + c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) + c.Abort() + return + } + c.Next() + } +} diff --git a/internal/api/v1/paths.go b/internal/api/v1/paths.go new file mode 100644 index 0000000..a18e452 --- /dev/null +++ b/internal/api/v1/paths.go @@ -0,0 +1,9 @@ +package api + +const ( + pathsPrefix = "/api/v1" + + pathsUser = pathsPrefix + "/user" + pathsUserRegister = pathsUser + "/register" + pathsUserLogin = pathsUser + "/login" +) diff --git a/internal/api/v1/services/auth_service.go b/internal/api/v1/services/auth_service.go new file mode 100644 index 0000000..5e568ea --- /dev/null +++ b/internal/api/v1/services/auth_service.go @@ -0,0 +1 @@ +package services diff --git a/internal/api/v1/services/resource_service.go b/internal/api/v1/services/resource_service.go new file mode 100644 index 0000000..5e568ea --- /dev/null +++ b/internal/api/v1/services/resource_service.go @@ -0,0 +1 @@ +package services diff --git a/internal/api/v1/services/user.go b/internal/api/v1/services/user.go new file mode 100644 index 0000000..ce2fbd1 --- /dev/null +++ b/internal/api/v1/services/user.go @@ -0,0 +1,76 @@ +package services + +import ( + "errors" + "time" + + "github.com/celestiaorg/knuu/internal/database/models" + "github.com/celestiaorg/knuu/internal/database/repos" + "github.com/golang-jwt/jwt/v4" + "golang.org/x/crypto/bcrypt" +) + +const ( + UserTokenDuration = 1 * time.Hour +) + +type UserService interface { + Register(user *models.User) (*models.User, error) + Authenticate(username, password string) (string, error) +} + +type userServiceImpl struct { + secretKey string + userRepo repos.UserRepository +} + +var _ UserService = &userServiceImpl{} + +// TODO: need to add the admin user for the first time +func NewUserService(secretKey string, userRepo repos.UserRepository) UserService { + return &userServiceImpl{ + secretKey: secretKey, + userRepo: userRepo, + } +} + +func (s *userServiceImpl) Register(user *models.User) (*models.User, error) { + if _, err := s.userRepo.FindUserByUsername(user.Username); err == nil { + return nil, errors.New("username already taken") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + user.Password = string(hashedPassword) + if err := s.userRepo.CreateUser(user); err != nil { + return nil, err + } + + return user, nil +} + +func (s *userServiceImpl) Authenticate(username, password string) (string, error) { + user, err := s.userRepo.FindUserByUsername(username) + if err != nil { + return "", err + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return "", errors.New("invalid credentials") + } + + // Generate JWT token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": user.ID, + "username": user.Username, + "role": user.Role, + "exp": time.Now().Add(UserTokenDuration).Unix(), + }) + tokenString, err := token.SignedString([]byte(s.secretKey)) + if err != nil { + return "", err + } + return tokenString, nil +} diff --git a/internal/database/db.go b/internal/database/db.go new file mode 100644 index 0000000..fdb3e06 --- /dev/null +++ b/internal/database/db.go @@ -0,0 +1,66 @@ +package database + +import ( + "fmt" + + "github.com/celestiaorg/knuu/internal/database/models" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +const ( + DefaultHost = "localhost" + DefaultUser = "postgres" + DefaultPassword = "postgres" + DefaultDBName = "postgres" + DefaultPort = 5432 +) + +type Options struct { + Host string + User string + Password string + DBName string + Port int +} + +func New(opts Options) (*gorm.DB, error) { + opts = setDefaults(opts) + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", + opts.Host, opts.User, opts.Password, opts.DBName, opts.Port) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, err + } + if err := migrate(db); err != nil { + return nil, err + } + return db, nil +} + +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 + } + return opts +} + +func migrate(db *gorm.DB) error { + return db.AutoMigrate( + &models.User{}, + &models.Token{}, + &models.Permission{}, + ) +} diff --git a/internal/database/models/test.go b/internal/database/models/test.go new file mode 100644 index 0000000..2640e7f --- /dev/null +++ b/internal/database/models/test.go @@ -0,0 +1 @@ +package models 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..4b467d3 --- /dev/null +++ b/internal/database/models/user.go @@ -0,0 +1,40 @@ +package models + +import "time" + +type UserRole int + +const ( + RoleAdmin UserRole = iota + 1 + RoleUser +) + +type User struct { + ID uint `json:"-" gorm:"primaryKey"` + Username string `json:"username" gorm:"unique;not null"` + Password string `json:"-" 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"` +} diff --git a/internal/database/repos/test.go b/internal/database/repos/test.go new file mode 100644 index 0000000..2b0d3a9 --- /dev/null +++ b/internal/database/repos/test.go @@ -0,0 +1 @@ +package repos 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..cc4164c --- /dev/null +++ b/internal/database/repos/user.go @@ -0,0 +1,51 @@ +package repos + +import ( + "github.com/celestiaorg/knuu/internal/database/models" + + "gorm.io/gorm" +) + +type UserRepository interface { + CreateUser(user *models.User) error + FindUserByUsername(username string) (*models.User, error) + FindUserByID(id uint) (*models.User, error) + UpdatePassword(id uint, password string) error + DeleteUserById(id uint) error +} + +type userRepositoryImpl struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepositoryImpl{db: db} +} + +func (r *userRepositoryImpl) CreateUser(user *models.User) error { + return r.db.Create(user).Error +} + +func (r *userRepositoryImpl) FindUserByUsername(username string) (*models.User, error) { + var user models.User + err := r.db.Where(&models.User{Username: username}).First(&user).Error + return &user, err +} + +func (r *userRepositoryImpl) FindUserByID(id uint) (*models.User, error) { + var user models.User + err := r.db.Where(&models.User{ID: id}).First(&user).Error + return &user, err +} + +func (r *userRepositoryImpl) UpdatePassword(id uint, password string) error { + updatedUser := &models.User{ + Password: password, + } + return r.db.Model(&models.User{}). + Where(&models.User{ID: id}).Updates(updatedUser).Error +} + +func (r *userRepositoryImpl) DeleteUserById(id uint) error { + return r.db.Delete(&models.User{ID: id}).Error +} From 3c0bd3b77fc4d85e7b485a4d0b1b761042bb2643 Mon Sep 17 00:00:00 2001 From: mojtaba-esk Date: Wed, 22 Jan 2025 18:06:10 +0330 Subject: [PATCH 2/8] feat: create test apis + db update + create instance --- cmd/api/api.go | 67 ++++---- go.mod | 3 +- go.sum | 2 + internal/api/v1/api.go | 53 +++++- internal/api/v1/handlers/instance.go | 44 +++++ internal/api/v1/handlers/test.go | 38 +++++ internal/api/v1/handlers/test_handler.go | 1 - internal/api/v1/handlers/token.go | 5 + internal/api/v1/handlers/token_handler.go | 1 - internal/api/v1/handlers/user.go | 21 ++- internal/api/v1/handlers/utils.go | 21 +++ internal/api/v1/index.go | 2 +- internal/api/v1/middleware/auth.go | 115 ++++++++++++- internal/api/v1/middleware/roles.go | 19 --- internal/api/v1/paths.go | 6 + internal/api/v1/services/auth_service.go | 1 - internal/api/v1/services/errors.go | 16 ++ internal/api/v1/services/instance.go | 105 ++++++++++++ internal/api/v1/services/resource_service.go | 1 - internal/api/v1/services/test.go | 161 +++++++++++++++++++ internal/api/v1/services/user.go | 72 ++++----- internal/database/db.go | 35 ++-- internal/database/models/test.go | 21 +++ internal/database/models/user.go | 6 +- internal/database/repos/test.go | 58 +++++++ internal/database/repos/user.go | 32 ++-- pkg/builder/git.go | 10 +- pkg/k8s/pod.go | 6 +- 28 files changed, 780 insertions(+), 142 deletions(-) create mode 100644 internal/api/v1/handlers/instance.go create mode 100644 internal/api/v1/handlers/test.go delete mode 100644 internal/api/v1/handlers/test_handler.go create mode 100644 internal/api/v1/handlers/token.go delete mode 100644 internal/api/v1/handlers/token_handler.go create mode 100644 internal/api/v1/handlers/utils.go delete mode 100644 internal/api/v1/middleware/roles.go delete mode 100644 internal/api/v1/services/auth_service.go create mode 100644 internal/api/v1/services/errors.go create mode 100644 internal/api/v1/services/instance.go delete mode 100644 internal/api/v1/services/resource_service.go create mode 100644 internal/api/v1/services/test.go diff --git a/cmd/api/api.go b/cmd/api/api.go index 8f21656..99efaad 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -1,6 +1,7 @@ package api import ( + "context" "fmt" "github.com/celestiaorg/knuu/internal/api/v1" @@ -66,16 +67,6 @@ func NewAPICmd() *cobra.Command { } func runAPIServer(cmd *cobra.Command, args []string) error { - port, err := cmd.Flags().GetInt(flagPort) - if err != nil { - return fmt.Errorf("failed to get port: %v", err) - } - - logLevel, err := cmd.Flags().GetString(flagLogLevel) - if err != nil { - return fmt.Errorf("failed to get log level: %v", err) - } - dbOpts, err := getDBOptions(cmd.Flags()) if err != nil { return fmt.Errorf("failed to get database options: %v", err) @@ -86,29 +77,16 @@ func runAPIServer(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to connect to database: %v", err) } - secretKey, err := cmd.Flags().GetString(flagSecretKey) + apiOpts, err := getAPIOptions(cmd.Flags()) if err != nil { - return fmt.Errorf("failed to get secret key: %v", err) + return fmt.Errorf("failed to get API options: %v", err) } - adminUser, err := cmd.Flags().GetString(flagAdminUser) + apiServer, err := api.New(context.Background(), db, apiOpts) if err != nil { - return fmt.Errorf("failed to get admin user: %v", err) + return fmt.Errorf("failed to create API server: %v", err) } - adminPass, err := cmd.Flags().GetString(flagAdminPass) - if err != nil { - return fmt.Errorf("failed to get admin password: %v", err) - } - - apiServer := api.New(db, api.Options{ - Port: port, - LogMode: logLevel, - SecretKey: secretKey, - AdminUser: adminUser, - AdminPass: adminPass, - }) - return apiServer.Start() } @@ -146,3 +124,38 @@ func getDBOptions(flags *pflag.FlagSet) (database.Options, error) { Port: dbPort, }, 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) + } + + logLevel, err := flags.GetString(flagLogLevel) + 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) + } + + return api.Options{ + Port: port, + LogMode: logLevel, + SecretKey: secretKey, + AdminUser: adminUser, + AdminPass: adminPass, + }, nil +} diff --git a/go.mod b/go.mod index b0c168e..a9011b1 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,14 @@ go 1.22.5 require ( github.com/celestiaorg/bittwister v0.0.0-20231213180407-65cdbaf5b8c7 github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt/v4 v4.5.1 github.com/google/uuid v1.6.0 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 @@ -78,7 +80,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect diff --git a/go.sum b/go.sum index 93ece83..ea9aba2 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe 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-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= diff --git a/internal/api/v1/api.go b/internal/api/v1/api.go index 8d7c82a..f4f4442 100644 --- a/internal/api/v1/api.go +++ b/internal/api/v1/api.go @@ -1,12 +1,14 @@ package api import ( + "context" "fmt" "net/http" "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" "github.com/gin-gonic/gin" @@ -29,21 +31,44 @@ type Options struct { LogMode string // gin.DebugMode, gin.ReleaseMode(default), gin.TestMode OriginAllowed string SecretKey string + + AdminUser string // default admin username + AdminPass string // default admin password } -func New(db *gorm.DB, opts Options) *API { +func New(ctx context.Context, db *gorm.DB, opts Options) (*API, error) { opts = setDefaults(opts) gin.SetMode(opts.LogMode) rt := gin.Default() + auth := middleware.NewAuth(opts.SecretKey) + uh, err := getUserHandler(ctx, opts, db, auth) + if err != nil { + return nil, err + } + public := rt.Group("/") { - uh := handlers.NewUserHandler(services.NewUserService(opts.SecretKey, repos.NewUserRepository(db))) - public.POST(pathsUserRegister, uh.Register) public.POST(pathsUserLogin, uh.Login) } - protected := rt.Group("/", middleware.AuthMiddleware()) + + protected := rt.Group("/", auth.AuthMiddleware()) + { + protected.POST(pathsUserRegister, auth.RequireRole(models.RoleAdmin), uh.Register) + + th, err := getTestHandler(ctx, db) + if err != nil { + return nil, err + } + + protected.POST(pathsTests, th.CreateTest) + // protected.GET(pathsTestDetails, th.GetTestDetails) + // protected.GET(pathsTestInstances, th.GetInstances) + protected.GET(pathsTestInstanceDetails, th.GetInstance) + protected.POST(pathsTestInstanceDetails, th.CreateInstance) // Need to do something about updating an instance + // protected.POST(pathsTestInstanceExecute, th.ExecuteInstance) + } _ = protected @@ -59,7 +84,7 @@ func New(db *gorm.DB, opts Options) *API { public.GET("/", a.IndexPage) } - return a + return a, nil } func (a *API) Start() error { @@ -103,3 +128,21 @@ func handleOrigin(router *gin.Engine, originAllowed string) http.Handler { AllowedMethods: methodsOk, }).Handler(router) } + +func getUserHandler(ctx context.Context, opts Options, db *gorm.DB, auth *middleware.Auth) (*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), nil +} + +func getTestHandler(ctx context.Context, db *gorm.DB) (*handlers.TestHandler, error) { + ts, err := services.NewTestService(ctx, repos.NewTestRepository(db)) + if err != nil { + return nil, err + } + + return handlers.NewTestHandler(ts), nil +} diff --git a/internal/api/v1/handlers/instance.go b/internal/api/v1/handlers/instance.go new file mode 100644 index 0000000..27ca523 --- /dev/null +++ b/internal/api/v1/handlers/instance.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "net/http" + + "github.com/celestiaorg/knuu/internal/api/v1/services" + "github.com/gin-gonic/gin" +) + +func (h *TestHandler) CreateInstance(c *gin.Context) { + user, err := getUserFromContext(c) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var input services.Instance + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) + return + } + + err = h.testService.CreateInstance(c.Request.Context(), user.ID, &input) + if err != nil { + 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) { + user, err := getUserFromContext(c) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + instance, err := h.testService.GetInstance(c.Request.Context(), user.ID, c.Param("scope"), c.Param("instance_name")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, instance) +} diff --git a/internal/api/v1/handlers/test.go b/internal/api/v1/handlers/test.go new file mode 100644 index 0000000..2037849 --- /dev/null +++ b/internal/api/v1/handlers/test.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "net/http" + + "github.com/celestiaorg/knuu/internal/api/v1/services" + "github.com/celestiaorg/knuu/internal/database/models" + "github.com/gin-gonic/gin" +) + +type TestHandler struct { + testService *services.TestService +} + +func NewTestHandler(ts *services.TestService) *TestHandler { + return &TestHandler{testService: ts} +} + +func (h *TestHandler) CreateTest(c *gin.Context) { + user, err := getUserFromContext(c) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + return + } + + var input models.Test + if err := c.ShouldBindJSON(&input); err != nil { + 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 { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, gin.H{"message": "Test created successfully"}) +} diff --git a/internal/api/v1/handlers/test_handler.go b/internal/api/v1/handlers/test_handler.go deleted file mode 100644 index 5ac8282..0000000 --- a/internal/api/v1/handlers/test_handler.go +++ /dev/null @@ -1 +0,0 @@ -package handlers 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/token_handler.go b/internal/api/v1/handlers/token_handler.go deleted file mode 100644 index 5ac8282..0000000 --- a/internal/api/v1/handlers/token_handler.go +++ /dev/null @@ -1 +0,0 @@ -package handlers diff --git a/internal/api/v1/handlers/user.go b/internal/api/v1/handlers/user.go index 35ea342..227ac96 100644 --- a/internal/api/v1/handlers/user.go +++ b/internal/api/v1/handlers/user.go @@ -3,6 +3,7 @@ package handlers import ( "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" @@ -11,10 +12,14 @@ import ( type UserHandler struct { userService services.UserService + auth *middleware.Auth } -func NewUserHandler(userService services.UserService) *UserHandler { - return &UserHandler{userService: userService} +func NewUserHandler(userService services.UserService, auth *middleware.Auth) *UserHandler { + return &UserHandler{ + userService: userService, + auth: auth, + } } func (h *UserHandler) Register(c *gin.Context) { @@ -24,12 +29,12 @@ func (h *UserHandler) Register(c *gin.Context) { return } - user, err := h.userService.Register(&input) + _, err := h.userService.Register(c.Request.Context(), &input) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusCreated, user) + c.JSON(http.StatusCreated, gin.H{"message": "User registered successfully"}) } func (h *UserHandler) Login(c *gin.Context) { @@ -42,10 +47,16 @@ func (h *UserHandler) Login(c *gin.Context) { return } - token, err := h.userService.Authenticate(input.Username, input.Password) + user, err := h.userService.Authenticate(c.Request.Context(), input.Username, input.Password) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } + + token, err := h.auth.GenerateToken(user) + if err != nil { + 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..0c8db9e --- /dev/null +++ b/internal/api/v1/handlers/utils.go @@ -0,0 +1,21 @@ +package handlers + +import ( + "errors" + + "github.com/celestiaorg/knuu/internal/api/v1/middleware" + "github.com/celestiaorg/knuu/internal/database/models" + "github.com/gin-gonic/gin" +) + +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 index 6c8fad9..c42fd37 100644 --- a/internal/api/v1/index.go +++ b/internal/api/v1/index.go @@ -46,7 +46,7 @@ func (a *API) IndexPage(c *gin.Context) { 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
`, href, a.Path) + html += fmt.Sprintf(`%s [ %s ]
`, href, a.Path, a.Method) } html += buildInfo diff --git a/internal/api/v1/middleware/auth.go b/internal/api/v1/middleware/auth.go index 65d69b8..0f286cb 100644 --- a/internal/api/v1/middleware/auth.go +++ b/internal/api/v1/middleware/auth.go @@ -1,24 +1,127 @@ package middleware import ( + "errors" "net/http" + "time" + "github.com/celestiaorg/knuu/internal/database/models" "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" ) -func AuthMiddleware() gin.HandlerFunc { +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 := c.GetHeader("Authorization") - if token == "" || !isValidToken(token) { + 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 isValidToken(token string) bool { - // Implement token validation logic - return token == "valid-token" +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/middleware/roles.go b/internal/api/v1/middleware/roles.go deleted file mode 100644 index 670bd55..0000000 --- a/internal/api/v1/middleware/roles.go +++ /dev/null @@ -1,19 +0,0 @@ -package middleware - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -func RequireRole(requiredRole string) gin.HandlerFunc { - return func(c *gin.Context) { - role := c.GetString("role") // Set during JWT parsing - if role != requiredRole { - c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) - c.Abort() - return - } - c.Next() - } -} diff --git a/internal/api/v1/paths.go b/internal/api/v1/paths.go index a18e452..e92c29c 100644 --- a/internal/api/v1/paths.go +++ b/internal/api/v1/paths.go @@ -6,4 +6,10 @@ const ( pathsUser = pathsPrefix + "/user" pathsUserRegister = pathsUser + "/register" pathsUserLogin = pathsUser + "/login" + + pathsTests = pathsPrefix + "/tests" + pathsTestDetails = pathsTests + "/{scope}" + pathsTestInstances = pathsTestDetails + "/instances" + pathsTestInstanceDetails = pathsTestInstances + "/{instance_id}" + pathsTestInstanceExecute = pathsTestInstanceDetails + "/execute" ) diff --git a/internal/api/v1/services/auth_service.go b/internal/api/v1/services/auth_service.go deleted file mode 100644 index 5e568ea..0000000 --- a/internal/api/v1/services/auth_service.go +++ /dev/null @@ -1 +0,0 @@ -package services diff --git a/internal/api/v1/services/errors.go b/internal/api/v1/services/errors.go new file mode 100644 index 0000000..fb52022 --- /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") + ErrInvalidCredentials = errors.New("InvalidCredentials", "invalid credentials") + ErrScopeRequired = errors.New("ScopeRequired", "scope is required") +) diff --git a/internal/api/v1/services/instance.go b/internal/api/v1/services/instance.go new file mode 100644 index 0000000..0e6cd80 --- /dev/null +++ b/internal/api/v1/services/instance.go @@ -0,0 +1,105 @@ +package services + +import ( + "context" + "fmt" + + "github.com/celestiaorg/knuu/pkg/builder" +) + +type Instance struct { + Name string `json:"name" binding:"required"` + Scope string `json:"scope" binding:"required"` + Image string `json:"image"` + GitContext builder.GitContext `json:"git_context"` + BuildArgs []string `json:"build_args"` + StartCommand []string `json:"start_command"` + Args []string `json:"args"` + Status string `json:"status"` // Readonly + 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 ins.Execution().StartAsync(ctx) + } + return nil +} + +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 + + return nil, fmt.Errorf("not implemented") +} diff --git a/internal/api/v1/services/resource_service.go b/internal/api/v1/services/resource_service.go deleted file mode 100644 index 5e568ea..0000000 --- a/internal/api/v1/services/resource_service.go +++ /dev/null @@ -1 +0,0 @@ -package services diff --git a/internal/api/v1/services/test.go b/internal/api/v1/services/test.go new file mode 100644 index 0000000..b678136 --- /dev/null +++ b/internal/api/v1/services/test.go @@ -0,0 +1,161 @@ +package services + +import ( + "context" + "fmt" + "sync" + "time" + + "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" + "github.com/sirupsen/logrus" +) + +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 +} + +func NewTestService(ctx context.Context, repo *repos.TestRepository) (*TestService, error) { + s := &TestService{ + repo: repo, + knuuList: make(map[uint]map[string]*knuu.Knuu), + } + + if err := s.loadKnuuFromDB(ctx); err != nil { + return nil, err + } + + return s, nil +} + +func (s *TestService) Create(ctx context.Context, test *models.Test) error { + if test.UserID == 0 { + return ErrUserIDRequired + } + + if err := s.prepareKnuu(ctx, test); err != nil { + return err + } + + return s.repo.Create(ctx, test) +} + +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 { + s.knuuListMu.Lock() + defer s.knuuListMu.Unlock() + + kn, ok := s.knuuList[userID][scope] + if !ok { + return nil + } + + if err := kn.CleanUp(ctx); err != nil { + return err + } + + delete(s.knuuList[userID], scope) + if len(s.knuuList[userID]) == 0 { + delete(s.knuuList, userID) + } + + 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(test *models.Test) error { + // Update the knuu object if needed e.g. deadline(timeout),... + // return s.repo.Update(test) + return fmt.Errorf("not implemented") +} + +func (s *TestService) loadKnuuFromDB(ctx context.Context) error { + tests, err := s.repo.ListAllAlive(ctx) + if err != nil { + return err + } + + for _, test := range tests { + 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 { + s.knuuListMu.Lock() + defer s.knuuListMu.Unlock() + + if _, ok := s.knuuList[test.UserID]; !ok { + s.knuuList[test.UserID] = make(map[string]*knuu.Knuu) + } + + if test.Scope == "" { + return ErrScopeRequired + } + + _, ok := s.knuuList[test.UserID][test.Scope] + if ok { + return ErrTestAlreadyExists + } + + var ( + logger = logrus.New() + minioClient *minio.Minio + ) + + k8sClient, err := k8s.NewClient(ctx, test.Scope, logger) + if err != nil { + return err + } + + if test.MinioEnabled { + minioClient, err = minio.New(ctx, k8sClient, logger) + if err != nil { + return err + } + } + + 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.knuuList[test.UserID][test.Scope] = kn + + return nil +} diff --git a/internal/api/v1/services/user.go b/internal/api/v1/services/user.go index ce2fbd1..d6eefe6 100644 --- a/internal/api/v1/services/user.go +++ b/internal/api/v1/services/user.go @@ -1,76 +1,76 @@ package services import ( - "errors" - "time" + "context" + "fmt" "github.com/celestiaorg/knuu/internal/database/models" "github.com/celestiaorg/knuu/internal/database/repos" - "github.com/golang-jwt/jwt/v4" "golang.org/x/crypto/bcrypt" ) -const ( - UserTokenDuration = 1 * time.Hour -) - type UserService interface { - Register(user *models.User) (*models.User, error) - Authenticate(username, password string) (string, error) + Register(ctx context.Context, user *models.User) (*models.User, error) + Authenticate(ctx context.Context, username, password string) (*models.User, error) } type userServiceImpl struct { - secretKey string - userRepo repos.UserRepository + repo repos.UserRepository } var _ UserService = &userServiceImpl{} -// TODO: need to add the admin user for the first time -func NewUserService(secretKey string, userRepo repos.UserRepository) UserService { - return &userServiceImpl{ - secretKey: secretKey, - userRepo: userRepo, +// 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(user *models.User) (*models.User, error) { - if _, err := s.userRepo.FindUserByUsername(user.Username); err == nil { - return nil, errors.New("username already taken") +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 } + fmt.Printf("user: %#v\n", user) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { return nil, err } user.Password = string(hashedPassword) - if err := s.userRepo.CreateUser(user); err != nil { + if err := s.repo.CreateUser(ctx, user); err != nil { return nil, err } return user, nil } -func (s *userServiceImpl) Authenticate(username, password string) (string, error) { - user, err := s.userRepo.FindUserByUsername(username) +func (s *userServiceImpl) Authenticate(ctx context.Context, username, password string) (*models.User, error) { + user, err := s.repo.FindUserByUsername(ctx, username) if err != nil { - return "", err + return nil, err } + + fmt.Printf("user.Password: `%s`\n", user.Password) + fmt.Printf("password: `%s`\n", password) if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - return "", errors.New("invalid credentials") + return nil, ErrInvalidCredentials.Wrap(err) } - // Generate JWT token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user_id": user.ID, - "username": user.Username, - "role": user.Role, - "exp": time.Now().Add(UserTokenDuration).Unix(), - }) - tokenString, err := token.SignedString([]byte(s.secretKey)) - if err != nil { - return "", err - } - return tokenString, nil + return user, nil } diff --git a/internal/database/db.go b/internal/database/db.go index fdb3e06..90450e1 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -9,25 +9,31 @@ import ( ) const ( - DefaultHost = "localhost" - DefaultUser = "postgres" - DefaultPassword = "postgres" - DefaultDBName = "postgres" - DefaultPort = 5432 + 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 + Host string + User string + Password string + DBName string + Port int + SSLEnabled *bool } func New(opts Options) (*gorm.DB, error) { opts = setDefaults(opts) - dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", - opts.Host, opts.User, opts.Password, opts.DBName, opts.Port) + 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 @@ -54,6 +60,10 @@ func setDefaults(opts Options) Options { if opts.Port == 0 { opts.Port = DefaultPort } + if opts.SSLEnabled == nil { + sslMode := DefaultSSLEnabled + opts.SSLEnabled = &sslMode + } return opts } @@ -62,5 +72,6 @@ func migrate(db *gorm.DB) error { &models.User{}, &models.Token{}, &models.Permission{}, + &models.Test{}, ) } diff --git a/internal/database/models/test.go b/internal/database/models/test.go index 2640e7f..a7fe1e7 100644 --- a/internal/database/models/test.go +++ b/internal/database/models/test.go @@ -1 +1,22 @@ package models + +import ( + "time" +) + +const ( + TestFinishedField = "finished" + TestCreatedAtField = "created_at" +) + +type Test struct { + Scope string `json:"scope" gorm:"primaryKey"` + 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"` +} diff --git a/internal/database/models/user.go b/internal/database/models/user.go index 4b467d3..27e304b 100644 --- a/internal/database/models/user.go +++ b/internal/database/models/user.go @@ -5,14 +5,14 @@ import "time" type UserRole int const ( - RoleAdmin UserRole = iota + 1 - RoleUser + RoleUser UserRole = iota + RoleAdmin ) type User struct { ID uint `json:"-" gorm:"primaryKey"` Username string `json:"username" gorm:"unique;not null"` - Password string `json:"-" gorm:"not null"` + Password string `json:"password" gorm:"not null"` Role UserRole `json:"role" gorm:"not null"` CreatedAt time.Time `json:"created_at"` } diff --git a/internal/database/repos/test.go b/internal/database/repos/test.go index 2b0d3a9..0ce02fe 100644 --- a/internal/database/repos/test.go +++ b/internal/database/repos/test.go @@ -1 +1,59 @@ package repos + +import ( + "context" + + "github.com/celestiaorg/knuu/internal/database/models" + "gorm.io/gorm" +) + +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}).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/user.go b/internal/database/repos/user.go index cc4164c..30514c8 100644 --- a/internal/database/repos/user.go +++ b/internal/database/repos/user.go @@ -1,17 +1,19 @@ package repos import ( + "context" + "github.com/celestiaorg/knuu/internal/database/models" "gorm.io/gorm" ) type UserRepository interface { - CreateUser(user *models.User) error - FindUserByUsername(username string) (*models.User, error) - FindUserByID(id uint) (*models.User, error) - UpdatePassword(id uint, password string) error - DeleteUserById(id uint) error + 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 { @@ -22,30 +24,30 @@ func NewUserRepository(db *gorm.DB) UserRepository { return &userRepositoryImpl{db: db} } -func (r *userRepositoryImpl) CreateUser(user *models.User) error { - return r.db.Create(user).Error +func (r *userRepositoryImpl) CreateUser(ctx context.Context, user *models.User) error { + return r.db.WithContext(ctx).Create(user).Error } -func (r *userRepositoryImpl) FindUserByUsername(username string) (*models.User, error) { +func (r *userRepositoryImpl) FindUserByUsername(ctx context.Context, username string) (*models.User, error) { var user models.User - err := r.db.Where(&models.User{Username: username}).First(&user).Error + err := r.db.WithContext(ctx).Where(&models.User{Username: username}).First(&user).Error return &user, err } -func (r *userRepositoryImpl) FindUserByID(id uint) (*models.User, error) { +func (r *userRepositoryImpl) FindUserByID(ctx context.Context, id uint) (*models.User, error) { var user models.User - err := r.db.Where(&models.User{ID: id}).First(&user).Error + err := r.db.WithContext(ctx).Where(&models.User{ID: id}).First(&user).Error return &user, err } -func (r *userRepositoryImpl) UpdatePassword(id uint, password string) error { +func (r *userRepositoryImpl) UpdatePassword(ctx context.Context, id uint, password string) error { updatedUser := &models.User{ Password: password, } - return r.db.Model(&models.User{}). + return r.db.WithContext(ctx).Model(&models.User{}). Where(&models.User{ID: id}).Updates(updatedUser).Error } -func (r *userRepositoryImpl) DeleteUserById(id uint) error { - return r.db.Delete(&models.User{ID: id}).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/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 { From 6c743934d23f508d1434a31f5c06874a00ed5fea Mon Sep 17 00:00:00 2001 From: mojtaba-esk Date: Fri, 24 Jan 2025 15:43:11 +0330 Subject: [PATCH 3/8] handle the start recovery and cleanup --- cmd/api/api.go | 25 ++- internal/api/v1/api.go | 43 +++-- internal/api/v1/handlers/instance.go | 17 +- internal/api/v1/paths.go | 5 +- internal/api/v1/services/instance.go | 25 ++- internal/api/v1/services/test.go | 224 +++++++++++++++++++++++---- internal/database/repos/test.go | 2 +- pkg/k8s/namespace.go | 2 +- 8 files changed, 286 insertions(+), 57 deletions(-) diff --git a/cmd/api/api.go b/cmd/api/api.go index 99efaad..cbb692a 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/celestiaorg/knuu/internal/api/v1" + "github.com/celestiaorg/knuu/internal/api/v1/services" "github.com/celestiaorg/knuu/internal/database" "github.com/gin-gonic/gin" @@ -28,18 +29,22 @@ const ( flagAdminUser = "admin-user" flagAdminPass = "admin-pass" + flagLogsPath = "logs-path" + defaultPort = 8080 defaultLogLevel = gin.ReleaseMode - defaultDBHost = "localhost" - defaultDBUser = "postgres" - defaultDBPassword = "postgres" - defaultDBName = "postgres" - defaultDBPort = 5432 + defaultDBHost = database.DefaultHost + defaultDBUser = database.DefaultUser + defaultDBPassword = database.DefaultPassword + defaultDBName = database.DefaultDBName + defaultDBPort = database.DefaultPort defaultSecretKey = "secret" defaultAdminUser = "admin" defaultAdminPass = "admin" + + defaultLogsPath = services.DefaultLogsPath ) func NewAPICmd() *cobra.Command { @@ -63,6 +68,8 @@ func NewAPICmd() *cobra.Command { apiCmd.Flags().StringP(flagAdminUser, "", defaultAdminUser, "Admin username") apiCmd.Flags().StringP(flagAdminPass, "", defaultAdminPass, "Admin password") + apiCmd.Flags().StringP(flagLogsPath, "", defaultLogsPath, "Path to store logs") + return apiCmd } @@ -151,11 +158,19 @@ func getAPIOptions(flags *pflag.FlagSet) (api.Options, error) { return api.Options{}, fmt.Errorf("failed to get admin password: %v", err) } + logsPath, err := flags.GetString(flagLogsPath) + if err != nil { + return api.Options{}, fmt.Errorf("failed to get logs path: %v", err) + } + return api.Options{ Port: port, LogMode: logLevel, SecretKey: secretKey, AdminUser: adminUser, AdminPass: adminPass, + TestServiceOptions: services.TestServiceOptions{ + LogsPath: logsPath, + }, }, nil } diff --git a/internal/api/v1/api.go b/internal/api/v1/api.go index f4f4442..f11fc39 100644 --- a/internal/api/v1/api.go +++ b/internal/api/v1/api.go @@ -21,9 +21,14 @@ const ( defaultLogMode = gin.ReleaseMode ) +type apiCleanup struct { + testService *services.TestService +} + type API struct { - router *gin.Engine - server *http.Server + router *gin.Engine + server *http.Server + cleanup apiCleanup } type Options struct { @@ -34,6 +39,8 @@ type Options struct { AdminUser string // default admin username AdminPass string // default admin password + + TestServiceOptions services.TestServiceOptions } func New(ctx context.Context, db *gorm.DB, opts Options) (*API, error) { @@ -53,19 +60,21 @@ func New(ctx context.Context, db *gorm.DB, opts Options) (*API, error) { 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, err := getTestHandler(ctx, db) - if err != nil { - return nil, err - } - + th := handlers.NewTestHandler(testService) protected.POST(pathsTests, th.CreateTest) // protected.GET(pathsTestDetails, th.GetTestDetails) // protected.GET(pathsTestInstances, th.GetInstances) protected.GET(pathsTestInstanceDetails, th.GetInstance) + protected.GET(pathsTestInstanceStatus, th.GetInstanceStatus) protected.POST(pathsTestInstanceDetails, th.CreateInstance) // Need to do something about updating an instance // protected.POST(pathsTestInstanceExecute, th.ExecuteInstance) } @@ -78,6 +87,9 @@ func New(ctx context.Context, db *gorm.DB, opts Options) (*API, error) { Addr: fmt.Sprintf(":%d", opts.Port), Handler: handleOrigin(rt, opts.OriginAllowed), }, + cleanup: apiCleanup{ + testService: testService, + }, } if opts.LogMode != gin.ReleaseMode { @@ -92,8 +104,14 @@ func (a *API) Start() error { return a.server.ListenAndServe() } -func (a *API) Stop() error { +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() } @@ -137,12 +155,3 @@ func getUserHandler(ctx context.Context, opts Options, db *gorm.DB, auth *middle return handlers.NewUserHandler(us, auth), nil } - -func getTestHandler(ctx context.Context, db *gorm.DB) (*handlers.TestHandler, error) { - ts, err := services.NewTestService(ctx, repos.NewTestRepository(db)) - if err != nil { - return nil, err - } - - return handlers.NewTestHandler(ts), nil -} diff --git a/internal/api/v1/handlers/instance.go b/internal/api/v1/handlers/instance.go index 27ca523..7e4c04b 100644 --- a/internal/api/v1/handlers/instance.go +++ b/internal/api/v1/handlers/instance.go @@ -35,10 +35,25 @@ func (h *TestHandler) GetInstance(c *gin.Context) { return } - instance, err := h.testService.GetInstance(c.Request.Context(), user.ID, c.Param("scope"), c.Param("instance_name")) + instance, err := h.testService.GetInstance(c.Request.Context(), user.ID, c.Param("scope"), c.Param("name")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, instance) } + +func (h *TestHandler) GetInstanceStatus(c *gin.Context) { + user, err := getUserFromContext(c) + if err != nil { + 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 { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": status}) +} diff --git a/internal/api/v1/paths.go b/internal/api/v1/paths.go index e92c29c..2dcab18 100644 --- a/internal/api/v1/paths.go +++ b/internal/api/v1/paths.go @@ -10,6 +10,9 @@ const ( pathsTests = pathsPrefix + "/tests" pathsTestDetails = pathsTests + "/{scope}" pathsTestInstances = pathsTestDetails + "/instances" - pathsTestInstanceDetails = pathsTestInstances + "/{instance_id}" + pathsTestInstanceDetails = pathsTestInstances + "/{name}" + pathsTestInstanceStatus = pathsTestInstanceDetails + "/status" + pathsTestInstanceLogs = pathsTestInstanceDetails + "/logs" + pathsTestInstanceStop = pathsTestInstanceDetails + "/stop" pathsTestInstanceExecute = pathsTestInstanceDetails + "/execute" ) diff --git a/internal/api/v1/services/instance.go b/internal/api/v1/services/instance.go index 0e6cd80..5d41f03 100644 --- a/internal/api/v1/services/instance.go +++ b/internal/api/v1/services/instance.go @@ -15,7 +15,6 @@ type Instance struct { BuildArgs []string `json:"build_args"` StartCommand []string `json:"start_command"` Args []string `json:"args"` - Status string `json:"status"` // Readonly StartNow bool `json:"start_now"` Env map[string]string `json:"env"` TCPPorts []int `json:"tcp_ports"` @@ -87,10 +86,14 @@ func (s *TestService) CreateInstance(ctx context.Context, userID uint, instance } } - if instance.StartNow { - return ins.Execution().StartAsync(ctx) + if !instance.StartNow { + return nil } - 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) { @@ -103,3 +106,17 @@ func (s *TestService) GetInstance(ctx context.Context, userID uint, scope, insta return nil, fmt.Errorf("not implemented") } + +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 +} diff --git a/internal/api/v1/services/test.go b/internal/api/v1/services/test.go index b678136..d5d7816 100644 --- a/internal/api/v1/services/test.go +++ b/internal/api/v1/services/test.go @@ -3,6 +3,8 @@ package services import ( "context" "fmt" + "os" + "path/filepath" "sync" "time" @@ -14,22 +16,65 @@ import ( "github.com/sirupsen/logrus" ) +const ( + DefaultTestTimeout = time.Hour * 1 + DefaultNamespace = "default" + DefaultLogsPath = "/tmp/knuu-logs" + 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 + 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 + logsPath string + cleanup *testServiceCleanup + logger *logrus.Logger + stopCleanupChan chan struct{} } -func NewTestService(ctx context.Context, repo *repos.TestRepository) (*TestService, error) { +type TestServiceOptions struct { + LogsPath string + Logger *logrus.Logger +} + +func NewTestService(ctx context.Context, repo *repos.TestRepository, opts TestServiceOptions) (*TestService, error) { + if opts.Logger == nil { + opts.Logger = logrus.New() + } + s := &TestService{ - repo: repo, - knuuList: make(map[uint]map[string]*knuu.Knuu), + repo: repo, + knuuList: make(map[uint]map[string]*knuu.Knuu), + logsPath: opts.LogsPath, + logger: opts.Logger, + stopCleanupChan: make(chan struct{}), + } + + if _, err := os.Stat(s.logsPath); os.IsNotExist(err) { + if err := os.MkdirAll(s.logsPath, 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.loadKnuuFromDB(ctx); err != nil { return nil, err } + go s.startPeriodicCleanup() return s, nil } @@ -58,23 +103,10 @@ func (s *TestService) Knuu(userID uint, scope string) (*knuu.Knuu, error) { } func (s *TestService) Delete(ctx context.Context, userID uint, scope string) error { - s.knuuListMu.Lock() - defer s.knuuListMu.Unlock() - - kn, ok := s.knuuList[userID][scope] - if !ok { - return nil - } - - if err := kn.CleanUp(ctx); err != nil { + if err := s.forceCleanupTest(ctx, userID, scope); err != nil { return err } - delete(s.knuuList[userID], scope) - if len(s.knuuList[userID]) == 0 { - delete(s.knuuList, userID) - } - return s.repo.Delete(ctx, scope) } @@ -90,10 +122,89 @@ func (s *TestService) Count(ctx context.Context, userID uint) (int64, error) { return s.repo.Count(ctx, userID) } -func (s *TestService) Update(test *models.Test) error { - // Update the knuu object if needed e.g. deadline(timeout),... - // return s.repo.Update(test) - return fmt.Errorf("not implemented") +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 _, logFile := range s.cleanup.logFiles { + if logFile == nil { + continue + } + + if err := logFile.Close(); err != nil { + return err + } + } + s.cleanup.logFiles = nil + + for userID, users := range s.knuuList { + for scope := range users { + if err := s.cleanupIfFinishedTest(ctx, userID, scope); err != nil { + return err + } + } + } + + return 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) { + ns, err := s.defaultK8sClient.GetNamespace(ctx, scope) + if err != nil { + return false, err + } + return ns != nil, nil } func (s *TestService) loadKnuuFromDB(ctx context.Context) error { @@ -103,7 +214,15 @@ func (s *TestService) loadKnuuFromDB(ctx context.Context) error { } for _, test := range tests { - err := s.prepareKnuu(ctx, &test) + 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 } @@ -114,11 +233,10 @@ func (s *TestService) loadKnuuFromDB(ctx context.Context) error { func (s *TestService) prepareKnuu(ctx context.Context, test *models.Test) error { s.knuuListMu.Lock() - defer s.knuuListMu.Unlock() - if _, ok := s.knuuList[test.UserID]; !ok { s.knuuList[test.UserID] = make(map[string]*knuu.Knuu) } + s.knuuListMu.Unlock() if test.Scope == "" { return ErrScopeRequired @@ -129,10 +247,20 @@ func (s *TestService) prepareKnuu(ctx context.Context, test *models.Test) error return ErrTestAlreadyExists } + logFile, err := os.OpenFile( + filepath.Join(s.logsPath, fmt.Sprintf("%s.log", test.Scope)), + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + LogsFilePermission, + ) + if err != nil { + return err + } + var ( logger = logrus.New() minioClient *minio.Minio ) + logger.SetOutput(logFile) k8sClient, err := k8s.NewClient(ctx, test.Scope, logger) if err != nil { @@ -146,6 +274,10 @@ func (s *TestService) prepareKnuu(ctx context.Context, test *models.Test) error } } + if test.Deadline.IsZero() { + test.Deadline = time.Now().Add(DefaultTestTimeout) + } + kn, err := knuu.New(ctx, knuu.Options{ ProxyEnabled: test.ProxyEnabled, K8sClient: k8sClient, @@ -155,7 +287,45 @@ func (s *TestService) prepareKnuu(ctx context.Context, test *models.Test) error 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/database/repos/test.go b/internal/database/repos/test.go index 0ce02fe..2945097 100644 --- a/internal/database/repos/test.go +++ b/internal/database/repos/test.go @@ -32,7 +32,7 @@ func (r *TestRepository) Delete(ctx context.Context, scope string) 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}).Updates(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) { diff --git a/pkg/k8s/namespace.go b/pkg/k8s/namespace.go index 308922e..7e594af 100644 --- a/pkg/k8s/namespace.go +++ b/pkg/k8s/namespace.go @@ -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 From 9b327e3ed955f8c3a23ca023bd43b70ecbdd0569 Mon Sep 17 00:00:00 2001 From: mojtaba-esk Date: Fri, 31 Jan 2025 12:24:36 +0330 Subject: [PATCH 4/8] feat: fixed create test and added logs and instance creation --- Makefile | 2 +- cmd/api/api.go | 62 ++++++++++++---- go.mod | 2 +- go.sum | 4 +- internal/api/v1/api.go | 37 ++++++---- internal/api/v1/handlers/errors.go | 9 +++ internal/api/v1/handlers/instance.go | 57 ++++++++++++++- internal/api/v1/handlers/test.go | 68 +++++++++++++++++- internal/api/v1/handlers/user.go | 34 ++++++++- internal/api/v1/paths.go | 5 +- internal/api/v1/services/errors.go | 2 +- internal/api/v1/services/instance.go | 21 +++++- internal/api/v1/services/test.go | 102 ++++++++++++++++++++------- internal/api/v1/services/user.go | 7 +- internal/database/db.go | 14 ++++ internal/database/models/test.go | 3 +- 16 files changed, 351 insertions(+), 78 deletions(-) create mode 100644 internal/api/v1/handlers/errors.go diff --git a/Makefile b/Makefile index 41b381e..e9ca905 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ build: # docker build -t $(BINARY_NAME) . run: build - ./bin/$(BINARY_NAME) api -l test + ./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 index cbb692a..e9dde26 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -7,6 +7,8 @@ import ( "github.com/celestiaorg/knuu/internal/api/v1" "github.com/celestiaorg/knuu/internal/api/v1/services" "github.com/celestiaorg/knuu/internal/database" + "github.com/sirupsen/logrus" + "gorm.io/gorm/logger" "github.com/gin-gonic/gin" "github.com/spf13/cobra" @@ -16,8 +18,8 @@ import ( const ( apiCmdName = "api" - flagPort = "port" - flagLogLevel = "log-level" + flagPort = "port" + flagAPILogLevel = "log-level" flagDBHost = "db.host" flagDBUser = "db.user" @@ -29,7 +31,7 @@ const ( flagAdminUser = "admin-user" flagAdminPass = "admin-pass" - flagLogsPath = "logs-path" + flagTestsLogsPath = "tests-logs-path" defaultPort = 8080 defaultLogLevel = gin.ReleaseMode @@ -44,7 +46,7 @@ const ( defaultAdminUser = "admin" defaultAdminPass = "admin" - defaultLogsPath = services.DefaultLogsPath + defaultLogsPath = services.DefaultTestLogsPath ) func NewAPICmd() *cobra.Command { @@ -56,7 +58,7 @@ func NewAPICmd() *cobra.Command { } apiCmd.Flags().IntP(flagPort, "p", defaultPort, "Port to run the API server on") - apiCmd.Flags().StringP(flagLogLevel, "l", defaultLogLevel, "Log level: debug | release | test") + 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") @@ -68,7 +70,7 @@ func NewAPICmd() *cobra.Command { apiCmd.Flags().StringP(flagAdminUser, "", defaultAdminUser, "Admin username") apiCmd.Flags().StringP(flagAdminPass, "", defaultAdminPass, "Admin password") - apiCmd.Flags().StringP(flagLogsPath, "", defaultLogsPath, "Path to store logs") + apiCmd.Flags().StringP(flagTestsLogsPath, "", defaultLogsPath, "Directory to store logs of the tests") return apiCmd } @@ -123,12 +125,28 @@ func getDBOptions(flags *pflag.FlagSet) (database.Options, error) { 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 } @@ -138,7 +156,7 @@ func getAPIOptions(flags *pflag.FlagSet) (api.Options, error) { return api.Options{}, fmt.Errorf("failed to get port: %v", err) } - logLevel, err := flags.GetString(flagLogLevel) + apiLogLevel, err := flags.GetString(flagAPILogLevel) if err != nil { return api.Options{}, fmt.Errorf("failed to get log level: %v", err) } @@ -158,19 +176,33 @@ func getAPIOptions(flags *pflag.FlagSet) (api.Options, error) { return api.Options{}, fmt.Errorf("failed to get admin password: %v", err) } - logsPath, err := flags.GetString(flagLogsPath) + testsLogsPath, err := flags.GetString(flagTestsLogsPath) if err != nil { - return api.Options{}, fmt.Errorf("failed to get logs path: %v", err) + 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, - LogMode: logLevel, - SecretKey: secretKey, - AdminUser: adminUser, - AdminPass: adminPass, + Port: port, + APILogMode: apiLogLevel, // gin logger (HTTP request level) + SecretKey: secretKey, + AdminUser: adminUser, + AdminPass: adminPass, + Logger: logger, // handler (application level logger) TestServiceOptions: services.TestServiceOptions{ - LogsPath: logsPath, + TestsLogsPath: testsLogsPath, // directory to store logs of each test + Logger: logger, }, }, nil } diff --git a/go.mod b/go.mod index a9011b1..97541e7 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/celestiaorg/bittwister v0.0.0-20231213180407-65cdbaf5b8c7 github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/golang-jwt/jwt/v4 v4.5.1 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 diff --git a/go.sum b/go.sum index ea9aba2..40d42ea 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,6 @@ 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-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 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= @@ -138,6 +136,8 @@ 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= diff --git a/internal/api/v1/api.go b/internal/api/v1/api.go index f11fc39..87b35f3 100644 --- a/internal/api/v1/api.go +++ b/internal/api/v1/api.go @@ -10,6 +10,7 @@ import ( "github.com/celestiaorg/knuu/internal/api/v1/services" "github.com/celestiaorg/knuu/internal/database/models" "github.com/celestiaorg/knuu/internal/database/repos" + "github.com/sirupsen/logrus" "github.com/gin-gonic/gin" "github.com/rs/cors" @@ -33,24 +34,25 @@ type API struct { type Options struct { Port int - LogMode string // gin.DebugMode, gin.ReleaseMode(default), gin.TestMode 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.LogMode) + gin.SetMode(opts.APILogMode) rt := gin.Default() auth := middleware.NewAuth(opts.SecretKey) - uh, err := getUserHandler(ctx, opts, db, auth) + uh, err := getUserHandler(ctx, opts, db, auth, opts.Logger) if err != nil { return nil, err } @@ -60,7 +62,10 @@ func New(ctx context.Context, db *gorm.DB, opts Options) (*API, error) { public.POST(pathsUserLogin, uh.Login) } - testService, err := services.NewTestService(ctx, repos.NewTestRepository(db), opts.TestServiceOptions) + testService, err := services.NewTestService(ctx, + repos.NewTestRepository(db), + opts.TestServiceOptions, + ) if err != nil { return nil, err } @@ -69,14 +74,15 @@ func New(ctx context.Context, db *gorm.DB, opts Options) (*API, error) { { protected.POST(pathsUserRegister, auth.RequireRole(models.RoleAdmin), uh.Register) - th := handlers.NewTestHandler(testService) + th := handlers.NewTestHandler(testService, opts.Logger) protected.POST(pathsTests, th.CreateTest) - // protected.GET(pathsTestDetails, th.GetTestDetails) + 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(pathsTestInstanceDetails, th.CreateInstance) // Need to do something about updating an instance - // protected.POST(pathsTestInstanceExecute, th.ExecuteInstance) + protected.POST(pathsTestInstanceExecute, th.ExecuteInstance) } _ = protected @@ -92,7 +98,7 @@ func New(ctx context.Context, db *gorm.DB, opts Options) (*API, error) { }, } - if opts.LogMode != gin.ReleaseMode { + if opts.APILogMode != gin.ReleaseMode { public.GET("/", a.IndexPage) } @@ -120,14 +126,19 @@ func setDefaults(opts Options) Options { opts.Port = defaultPort } - if opts.LogMode == "" { - opts.LogMode = defaultLogMode + 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 } @@ -147,11 +158,11 @@ func handleOrigin(router *gin.Engine, originAllowed string) http.Handler { }).Handler(router) } -func getUserHandler(ctx context.Context, opts Options, db *gorm.DB, auth *middleware.Auth) (*handlers.UserHandler, error) { +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), nil + 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 index 7e4c04b..2d4a979 100644 --- a/internal/api/v1/handlers/instance.go +++ b/internal/api/v1/handlers/instance.go @@ -5,23 +5,34 @@ import ( "github.com/celestiaorg/knuu/internal/api/v1/services" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" ) 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 } @@ -29,31 +40,73 @@ func (h *TestHandler) CreateInstance(c *gin.Context) { } 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 { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return + 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 index 2037849..5e6b9a1 100644 --- a/internal/api/v1/handlers/test.go +++ b/internal/api/v1/handlers/test.go @@ -1,38 +1,100 @@ package handlers import ( + "errors" + "fmt" "net/http" "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 TestHandler struct { testService *services.TestService + logger *logrus.Logger } -func NewTestHandler(ts *services.TestService) *TestHandler { - return &TestHandler{testService: ts} +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 { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + 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/user.go b/internal/api/v1/handlers/user.go index 227ac96..1279b4d 100644 --- a/internal/api/v1/handlers/user.go +++ b/internal/api/v1/handlers/user.go @@ -1,11 +1,13 @@ 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/sirupsen/logrus" "github.com/gin-gonic/gin" ) @@ -13,48 +15,74 @@ import ( type UserHandler struct { userService services.UserService auth *middleware.Auth + logger *logrus.Logger } -func NewUserHandler(userService services.UserService, auth *middleware.Auth) *UserHandler { +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 { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + 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 { - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + 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 } diff --git a/internal/api/v1/paths.go b/internal/api/v1/paths.go index 2dcab18..8649194 100644 --- a/internal/api/v1/paths.go +++ b/internal/api/v1/paths.go @@ -8,9 +8,10 @@ const ( pathsUserLogin = pathsUser + "/login" pathsTests = pathsPrefix + "/tests" - pathsTestDetails = pathsTests + "/{scope}" + pathsTestDetails = pathsTests + "/:scope" + pathsTestLogs = pathsTestDetails + "/logs" pathsTestInstances = pathsTestDetails + "/instances" - pathsTestInstanceDetails = pathsTestInstances + "/{name}" + pathsTestInstanceDetails = pathsTestInstances + "/:name" pathsTestInstanceStatus = pathsTestInstanceDetails + "/status" pathsTestInstanceLogs = pathsTestInstanceDetails + "/logs" pathsTestInstanceStop = pathsTestInstanceDetails + "/stop" diff --git a/internal/api/v1/services/errors.go b/internal/api/v1/services/errors.go index fb52022..7649f72 100644 --- a/internal/api/v1/services/errors.go +++ b/internal/api/v1/services/errors.go @@ -11,6 +11,6 @@ var ( ErrUserIDRequired = errors.New("UserIDRequired", "user ID is required") ErrTestAlreadyExists = errors.New("TestAlreadyExists", "test already exists") ErrTestNotFound = errors.New("TestNotFound", "test not found") - ErrInvalidCredentials = errors.New("InvalidCredentials", "invalid credentials") 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 index 5d41f03..d121f35 100644 --- a/internal/api/v1/services/instance.go +++ b/internal/api/v1/services/instance.go @@ -9,7 +9,7 @@ import ( type Instance struct { Name string `json:"name" binding:"required"` - Scope string `json:"scope" binding:"required"` + Scope string `json:"scope"` Image string `json:"image"` GitContext builder.GitContext `json:"git_context"` BuildArgs []string `json:"build_args"` @@ -104,7 +104,11 @@ func (s *TestService) GetInstance(ctx context.Context, userID uint, scope, insta _ = kn - return nil, fmt.Errorf("not implemented") + 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) { @@ -120,3 +124,16 @@ func (s *TestService) GetInstanceStatus(ctx context.Context, userID uint, scope, 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 index d5d7816..36027e1 100644 --- a/internal/api/v1/services/test.go +++ b/internal/api/v1/services/test.go @@ -2,12 +2,16 @@ package services import ( "context" + "errors" "fmt" "os" "path/filepath" "sync" "time" + 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" @@ -19,7 +23,7 @@ import ( const ( DefaultTestTimeout = time.Hour * 1 DefaultNamespace = "default" - DefaultLogsPath = "/tmp/knuu-logs" + DefaultTestLogsPath = "/tmp/knuu-logs" // directory to store logs of each test LogsDirPermission = 0755 LogsFilePermission = 0644 PeriodicCleanupInterval = time.Minute * 10 @@ -34,32 +38,30 @@ type TestService struct { knuuList map[uint]map[string]*knuu.Knuu // key is the user ID, second key is the scope knuuListMu sync.RWMutex defaultK8sClient *k8s.Client - logsPath string + testsLogsPath string cleanup *testServiceCleanup logger *logrus.Logger stopCleanupChan chan struct{} } type TestServiceOptions struct { - LogsPath string - Logger *logrus.Logger + 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) { - if opts.Logger == nil { - opts.Logger = logrus.New() - } + opts = setServiceOptsDefaults(opts) s := &TestService{ repo: repo, knuuList: make(map[uint]map[string]*knuu.Knuu), - logsPath: opts.LogsPath, + testsLogsPath: opts.TestsLogsPath, logger: opts.Logger, stopCleanupChan: make(chan struct{}), } - if _, err := os.Stat(s.logsPath); os.IsNotExist(err) { - if err := os.MkdirAll(s.logsPath, LogsDirPermission); err != nil { + if _, err := os.Stat(s.testsLogsPath); os.IsNotExist(err) { + if err := os.MkdirAll(s.testsLogsPath, LogsDirPermission); err != nil { return nil, err } } @@ -70,7 +72,7 @@ func NewTestService(ctx context.Context, repo *repos.TestRepository, opts TestSe } s.defaultK8sClient = k8sClient - if err := s.loadKnuuFromDB(ctx); err != nil { + if err := s.loadRunningTestsFromDB(ctx); err != nil { return nil, err } @@ -78,16 +80,38 @@ func NewTestService(ctx context.Context, repo *repos.TestRepository, opts TestSe 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 } - if err := s.prepareKnuu(ctx, test); err != nil { + err := s.repo.Create(ctx, test) + if database.IsDuplicateKeyError(err) { + return ErrTestAlreadyExists + } else if err != nil { return err } - return s.repo.Create(ctx, test) + // 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) { @@ -163,6 +187,25 @@ func (s *TestService) Shutdown(ctx context.Context) error { 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 { @@ -200,14 +243,14 @@ func (s *TestService) forceCleanupTest(ctx context.Context, userID uint, scope s } func (s *TestService) isTestRunning(ctx context.Context, scope string) (bool, error) { - ns, err := s.defaultK8sClient.GetNamespace(ctx, scope) - if err != nil { - return false, err + _, err := s.defaultK8sClient.GetNamespace(ctx, scope) + if apierrs.IsNotFound(err) { + return false, nil } - return ns != nil, nil + return err == nil, err } -func (s *TestService) loadKnuuFromDB(ctx context.Context) error { +func (s *TestService) loadRunningTestsFromDB(ctx context.Context) error { tests, err := s.repo.ListAllAlive(ctx) if err != nil { return err @@ -248,27 +291,34 @@ func (s *TestService) prepareKnuu(ctx context.Context, test *models.Test) error } logFile, err := os.OpenFile( - filepath.Join(s.logsPath, fmt.Sprintf("%s.log", test.Scope)), + 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 } - var ( - logger = logrus.New() - minioClient *minio.Minio - ) - logger.SetOutput(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, logger) + 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, logger) + minioClient, err = minio.New(ctx, k8sClient, testLogger) if err != nil { return err } diff --git a/internal/api/v1/services/user.go b/internal/api/v1/services/user.go index d6eefe6..34c7ba3 100644 --- a/internal/api/v1/services/user.go +++ b/internal/api/v1/services/user.go @@ -2,7 +2,6 @@ package services import ( "context" - "fmt" "github.com/celestiaorg/knuu/internal/database/models" "github.com/celestiaorg/knuu/internal/database/repos" @@ -45,8 +44,6 @@ func (s *userServiceImpl) Register(ctx context.Context, user *models.User) (*mod return nil, ErrUsernameAlreadyTaken } - fmt.Printf("user: %#v\n", user) - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { return nil, err @@ -66,10 +63,8 @@ func (s *userServiceImpl) Authenticate(ctx context.Context, username, password s return nil, err } - fmt.Printf("user.Password: `%s`\n", user.Password) - fmt.Printf("password: `%s`\n", password) if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - return nil, ErrInvalidCredentials.Wrap(err) + return nil, err } return user, nil diff --git a/internal/database/db.go b/internal/database/db.go index 90450e1..c2f0449 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -1,11 +1,13 @@ package database import ( + "errors" "fmt" "github.com/celestiaorg/knuu/internal/database/models" "gorm.io/driver/postgres" "gorm.io/gorm" + "gorm.io/gorm/logger" ) const ( @@ -24,6 +26,7 @@ type Options struct { DBName string Port int SSLEnabled *bool + LogLevel logger.LogLevel } func New(opts Options) (*gorm.DB, error) { @@ -41,9 +44,17 @@ func New(opts Options) (*gorm.DB, error) { 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 @@ -64,6 +75,9 @@ func setDefaults(opts Options) Options { sslMode := DefaultSSLEnabled opts.SSLEnabled = &sslMode } + if opts.LogLevel == 0 { + opts.LogLevel = logger.Warn + } return opts } diff --git a/internal/database/models/test.go b/internal/database/models/test.go index a7fe1e7..5ce9283 100644 --- a/internal/database/models/test.go +++ b/internal/database/models/test.go @@ -10,7 +10,7 @@ const ( ) type Test struct { - Scope string `json:"scope" gorm:"primaryKey"` + 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:""` @@ -19,4 +19,5 @@ type Test struct { 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") } From ef82ce9c1ac8975a891441d4f3f8d424ed945050 Mon Sep 17 00:00:00 2001 From: mojtaba-esk Date: Fri, 31 Jan 2025 12:41:08 +0330 Subject: [PATCH 5/8] fixed linter complains and a failing test --- internal/api/v1/middleware/auth.go | 3 ++- internal/database/db.go | 3 ++- internal/database/repos/test.go | 3 ++- pkg/k8s/namespace_test.go | 10 ++++------ 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/internal/api/v1/middleware/auth.go b/internal/api/v1/middleware/auth.go index 0f286cb..dd39ca8 100644 --- a/internal/api/v1/middleware/auth.go +++ b/internal/api/v1/middleware/auth.go @@ -5,9 +5,10 @@ import ( "net/http" "time" - "github.com/celestiaorg/knuu/internal/database/models" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" + + "github.com/celestiaorg/knuu/internal/database/models" ) const ( diff --git a/internal/database/db.go b/internal/database/db.go index c2f0449..a946e99 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -4,10 +4,11 @@ import ( "errors" "fmt" - "github.com/celestiaorg/knuu/internal/database/models" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" + + "github.com/celestiaorg/knuu/internal/database/models" ) const ( diff --git a/internal/database/repos/test.go b/internal/database/repos/test.go index 2945097..760c53a 100644 --- a/internal/database/repos/test.go +++ b/internal/database/repos/test.go @@ -3,8 +3,9 @@ package repos import ( "context" - "github.com/celestiaorg/knuu/internal/database/models" "gorm.io/gorm" + + "github.com/celestiaorg/knuu/internal/database/models" ) type TestRepository struct { 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", From cf614d046c28b04be8505edf2f83ecfb9b789673 Mon Sep 17 00:00:00 2001 From: mojtaba-esk Date: Fri, 31 Jan 2025 12:51:20 +0330 Subject: [PATCH 6/8] fixed linter complains --- cmd/api/api.go | 12 ++++++------ cmd/main.go | 3 ++- internal/api/v1/api.go | 10 +++++----- internal/api/v1/handlers/instance.go | 3 ++- internal/api/v1/handlers/test.go | 5 +++-- internal/api/v1/handlers/user.go | 2 +- internal/api/v1/handlers/utils.go | 3 ++- internal/api/v1/services/test.go | 2 +- internal/api/v1/services/user.go | 3 ++- 9 files changed, 24 insertions(+), 19 deletions(-) diff --git a/cmd/api/api.go b/cmd/api/api.go index e9dde26..d474bd0 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -4,15 +4,15 @@ import ( "context" "fmt" - "github.com/celestiaorg/knuu/internal/api/v1" - "github.com/celestiaorg/knuu/internal/api/v1/services" - "github.com/celestiaorg/knuu/internal/database" - "github.com/sirupsen/logrus" - "gorm.io/gorm/logger" - "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" + "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 ( diff --git a/cmd/main.go b/cmd/main.go index b7ce832..2c79e61 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,8 +1,9 @@ package main import ( - "github.com/celestiaorg/knuu/cmd/root" "github.com/sirupsen/logrus" + + "github.com/celestiaorg/knuu/cmd/root" ) func main() { diff --git a/internal/api/v1/api.go b/internal/api/v1/api.go index 87b35f3..dc05113 100644 --- a/internal/api/v1/api.go +++ b/internal/api/v1/api.go @@ -5,16 +5,16 @@ import ( "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" - "github.com/sirupsen/logrus" - - "github.com/gin-gonic/gin" - "github.com/rs/cors" - "gorm.io/gorm" ) const ( diff --git a/internal/api/v1/handlers/instance.go b/internal/api/v1/handlers/instance.go index 2d4a979..cf0674e 100644 --- a/internal/api/v1/handlers/instance.go +++ b/internal/api/v1/handlers/instance.go @@ -3,9 +3,10 @@ package handlers import ( "net/http" - "github.com/celestiaorg/knuu/internal/api/v1/services" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + + "github.com/celestiaorg/knuu/internal/api/v1/services" ) func (h *TestHandler) CreateInstance(c *gin.Context) { diff --git a/internal/api/v1/handlers/test.go b/internal/api/v1/handlers/test.go index 5e6b9a1..4c23026 100644 --- a/internal/api/v1/handlers/test.go +++ b/internal/api/v1/handlers/test.go @@ -5,10 +5,11 @@ import ( "fmt" "net/http" - "github.com/celestiaorg/knuu/internal/api/v1/services" - "github.com/celestiaorg/knuu/internal/database/models" "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 { diff --git a/internal/api/v1/handlers/user.go b/internal/api/v1/handlers/user.go index 1279b4d..5afdd4d 100644 --- a/internal/api/v1/handlers/user.go +++ b/internal/api/v1/handlers/user.go @@ -7,9 +7,9 @@ import ( "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/sirupsen/logrus" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" ) type UserHandler struct { diff --git a/internal/api/v1/handlers/utils.go b/internal/api/v1/handlers/utils.go index 0c8db9e..9b5b381 100644 --- a/internal/api/v1/handlers/utils.go +++ b/internal/api/v1/handlers/utils.go @@ -3,9 +3,10 @@ package handlers import ( "errors" + "github.com/gin-gonic/gin" + "github.com/celestiaorg/knuu/internal/api/v1/middleware" "github.com/celestiaorg/knuu/internal/database/models" - "github.com/gin-gonic/gin" ) func getUserFromContext(c *gin.Context) (*models.User, error) { diff --git a/internal/api/v1/services/test.go b/internal/api/v1/services/test.go index 36027e1..1ea0bb2 100644 --- a/internal/api/v1/services/test.go +++ b/internal/api/v1/services/test.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/sirupsen/logrus" apierrs "k8s.io/apimachinery/pkg/api/errors" "github.com/celestiaorg/knuu/internal/database" @@ -17,7 +18,6 @@ import ( "github.com/celestiaorg/knuu/pkg/k8s" "github.com/celestiaorg/knuu/pkg/knuu" "github.com/celestiaorg/knuu/pkg/minio" - "github.com/sirupsen/logrus" ) const ( diff --git a/internal/api/v1/services/user.go b/internal/api/v1/services/user.go index 34c7ba3..d101ea9 100644 --- a/internal/api/v1/services/user.go +++ b/internal/api/v1/services/user.go @@ -3,9 +3,10 @@ package services import ( "context" + "golang.org/x/crypto/bcrypt" + "github.com/celestiaorg/knuu/internal/database/models" "github.com/celestiaorg/knuu/internal/database/repos" - "golang.org/x/crypto/bcrypt" ) type UserService interface { From 655d3020642d03fe0304de3ec1427b28139e5ff4 Mon Sep 17 00:00:00 2001 From: mojtaba-esk Date: Fri, 31 Jan 2025 16:33:28 +0330 Subject: [PATCH 7/8] fix: added a tests cope validation --- internal/api/v1/services/test.go | 4 ++++ pkg/k8s/namespace.go | 2 +- pkg/k8s/validate.go | 6 +++--- pkg/k8s/validate_test.go | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/api/v1/services/test.go b/internal/api/v1/services/test.go index 1ea0bb2..8ffa9f8 100644 --- a/internal/api/v1/services/test.go +++ b/internal/api/v1/services/test.go @@ -275,6 +275,10 @@ func (s *TestService) loadRunningTestsFromDB(ctx context.Context) error { } 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) diff --git a/pkg/k8s/namespace.go b/pkg/k8s/namespace.go index 7e594af..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 } 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) }) } From e3967d1558d3cfed1c717bc262f47b8501bbc675 Mon Sep 17 00:00:00 2001 From: mojtaba-esk Date: Fri, 31 Jan 2025 17:05:36 +0330 Subject: [PATCH 8/8] fix: the gracefull shutdown --- .gitignore | 1 + cmd/api/api.go | 30 ++++++++++++++++++++++++++++++ internal/api/v1/services/test.go | 21 +++++++++++++-------- internal/database/models/user.go | 20 +++++++++++++++++++- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 93fcc47..6886fea 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Taskfile.yaml bin/* docker-compose.yml .env +tmp.sh diff --git a/cmd/api/api.go b/cmd/api/api.go index d474bd0..63294e0 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -3,11 +3,15 @@ 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" @@ -96,6 +100,8 @@ func runAPIServer(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to create API server: %v", err) } + handleShutdown(apiServer, db, apiOpts.Logger) + return apiServer.Start() } @@ -206,3 +212,27 @@ func getAPIOptions(flags *pflag.FlagSet) (api.Options, error) { }, }, 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/internal/api/v1/services/test.go b/internal/api/v1/services/test.go index 8ffa9f8..6db4762 100644 --- a/internal/api/v1/services/test.go +++ b/internal/api/v1/services/test.go @@ -165,6 +165,18 @@ func (s *TestService) SetFinished(ctx context.Context, userID uint, scope string 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 @@ -176,14 +188,6 @@ func (s *TestService) Shutdown(ctx context.Context) error { } s.cleanup.logFiles = nil - for userID, users := range s.knuuList { - for scope := range users { - if err := s.cleanupIfFinishedTest(ctx, userID, scope); err != nil { - return err - } - } - } - return nil } @@ -303,6 +307,7 @@ func (s *TestService) prepareKnuu(ctx context.Context, test *models.Test) error 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) diff --git a/internal/database/models/user.go b/internal/database/models/user.go index 27e304b..9a98daa 100644 --- a/internal/database/models/user.go +++ b/internal/database/models/user.go @@ -1,6 +1,11 @@ package models -import "time" +import ( + "time" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) type UserRole int @@ -38,3 +43,16 @@ type Permission struct { 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 +}