Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first attempt for knuu API #601

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ coverage.txt
.devcontainer
Taskfile.yaml

bin/*
docker-compose.yml
.env
tmp.sh
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ pkgs := $(shell go list ./...)
run := .
count := 1
timeout := 120m
BINARY_NAME := knuu

build:
go build -o bin/$(BINARY_NAME) -v ./cmd

# docker:
# docker build -t $(BINARY_NAME) .

run: build
./bin/$(BINARY_NAME) api -l debug

## help: Show this help message
help: Makefile
Expand Down
238 changes: 238 additions & 0 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package api

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"gorm.io/gorm"
"gorm.io/gorm/logger"

"github.com/celestiaorg/knuu/internal/api/v1"
"github.com/celestiaorg/knuu/internal/api/v1/services"
"github.com/celestiaorg/knuu/internal/database"
)

const (
apiCmdName = "api"

flagPort = "port"
flagAPILogLevel = "log-level"

flagDBHost = "db.host"
flagDBUser = "db.user"
flagDBPassword = "db.password"
flagDBName = "db.name"
flagDBPort = "db.port"

flagSecretKey = "secret-key"
flagAdminUser = "admin-user"
flagAdminPass = "admin-pass"

flagTestsLogsPath = "tests-logs-path"

defaultPort = 8080
defaultLogLevel = gin.ReleaseMode

defaultDBHost = database.DefaultHost
defaultDBUser = database.DefaultUser
defaultDBPassword = database.DefaultPassword
defaultDBName = database.DefaultDBName
defaultDBPort = database.DefaultPort

defaultSecretKey = "secret"
defaultAdminUser = "admin"
defaultAdminPass = "admin"
Comment on lines +49 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security: Replace insecure default values.

The default values for sensitive data are insecure:

  • Default secret key is too simple
  • Default admin credentials are too weak

Consider:

  1. Requiring these values to be provided through environment variables
  2. Adding validation for minimum password complexity
  3. Generating a secure random secret key if not provided

Example implementation:

-    defaultSecretKey = "secret"
-    defaultAdminUser = "admin"
-    defaultAdminPass = "admin"
+    defaultSecretKey = "" // Must be provided
+    defaultAdminUser = "" // Must be provided
+    defaultAdminPass = "" // Must be provided
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defaultSecretKey = "secret"
defaultAdminUser = "admin"
defaultAdminPass = "admin"
defaultSecretKey = "" // Must be provided
defaultAdminUser = "" // Must be provided
defaultAdminPass = "" // Must be provided


defaultLogsPath = services.DefaultTestLogsPath
)

func NewAPICmd() *cobra.Command {
apiCmd := &cobra.Command{
Use: apiCmdName,
Short: "Start the Knuu API server",
Long: "Start the API server to manage tests, tokens, and users.",
RunE: runAPIServer,
}

apiCmd.Flags().IntP(flagPort, "p", defaultPort, "Port to run the API server on")
apiCmd.Flags().StringP(flagAPILogLevel, "l", defaultLogLevel, "Log level: debug | release | test")

apiCmd.Flags().StringP(flagDBHost, "d", defaultDBHost, "Postgres database host")
apiCmd.Flags().StringP(flagDBUser, "", defaultDBUser, "Postgres database user")
apiCmd.Flags().StringP(flagDBPassword, "", defaultDBPassword, "Postgres database password")
apiCmd.Flags().StringP(flagDBName, "", defaultDBName, "Postgres database name")
apiCmd.Flags().IntP(flagDBPort, "", defaultDBPort, "Postgres database port")

apiCmd.Flags().StringP(flagSecretKey, "", defaultSecretKey, "JWT secret key")
apiCmd.Flags().StringP(flagAdminUser, "", defaultAdminUser, "Admin username")
apiCmd.Flags().StringP(flagAdminPass, "", defaultAdminPass, "Admin password")

apiCmd.Flags().StringP(flagTestsLogsPath, "", defaultLogsPath, "Directory to store logs of the tests")

return apiCmd
}

func runAPIServer(cmd *cobra.Command, args []string) error {
dbOpts, err := getDBOptions(cmd.Flags())
if err != nil {
return fmt.Errorf("failed to get database options: %v", err)
}

db, err := database.New(dbOpts)
if err != nil {
return fmt.Errorf("failed to connect to database: %v", err)
}

apiOpts, err := getAPIOptions(cmd.Flags())
if err != nil {
return fmt.Errorf("failed to get API options: %v", err)
}

apiServer, err := api.New(context.Background(), db, apiOpts)
if err != nil {
return fmt.Errorf("failed to create API server: %v", err)
}

handleShutdown(apiServer, db, apiOpts.Logger)

return apiServer.Start()
}

func getDBOptions(flags *pflag.FlagSet) (database.Options, error) {
dbHost, err := flags.GetString(flagDBHost)
if err != nil {
return database.Options{}, fmt.Errorf("failed to get database host: %v", err)
}

dbUser, err := flags.GetString(flagDBUser)
if err != nil {
return database.Options{}, fmt.Errorf("failed to get database user: %v", err)
}

dbPassword, err := flags.GetString(flagDBPassword)
if err != nil {
return database.Options{}, fmt.Errorf("failed to get database password: %v", err)
}

dbName, err := flags.GetString(flagDBName)
if err != nil {
return database.Options{}, fmt.Errorf("failed to get database name: %v", err)
}

dbPort, err := flags.GetInt(flagDBPort)
if err != nil {
return database.Options{}, fmt.Errorf("failed to get database port: %v", err)
}

apiLogLevel, err := flags.GetString(flagAPILogLevel)
if err != nil {
return database.Options{}, fmt.Errorf("failed to get API log level: %v", err)
}

var dbLogLevel logger.LogLevel
switch apiLogLevel {
case gin.DebugMode:
dbLogLevel = logger.Info
case gin.ReleaseMode:
dbLogLevel = logger.Error
case gin.TestMode:
dbLogLevel = logger.Info
}

return database.Options{
Host: dbHost,
User: dbUser,
Password: dbPassword,
DBName: dbName,
Port: dbPort,
LogLevel: dbLogLevel,
}, nil
}

func getAPIOptions(flags *pflag.FlagSet) (api.Options, error) {
port, err := flags.GetInt(flagPort)
if err != nil {
return api.Options{}, fmt.Errorf("failed to get port: %v", err)
}

apiLogLevel, err := flags.GetString(flagAPILogLevel)
if err != nil {
return api.Options{}, fmt.Errorf("failed to get log level: %v", err)
}

secretKey, err := flags.GetString(flagSecretKey)
if err != nil {
return api.Options{}, fmt.Errorf("failed to get secret key: %v", err)
}

adminUser, err := flags.GetString(flagAdminUser)
if err != nil {
return api.Options{}, fmt.Errorf("failed to get admin user: %v", err)
}

adminPass, err := flags.GetString(flagAdminPass)
if err != nil {
return api.Options{}, fmt.Errorf("failed to get admin password: %v", err)
}

testsLogsPath, err := flags.GetString(flagTestsLogsPath)
if err != nil {
return api.Options{}, fmt.Errorf("failed to get tests logs path: %v", err)
}

logger := logrus.New()
logger.SetFormatter(&logrus.JSONFormatter{})

switch apiLogLevel {
case gin.DebugMode:
logger.SetLevel(logrus.DebugLevel)
case gin.ReleaseMode:
logger.SetLevel(logrus.ErrorLevel)
case gin.TestMode:
logger.SetLevel(logrus.InfoLevel)
}

return api.Options{
Port: port,
APILogMode: apiLogLevel, // gin logger (HTTP request level)
SecretKey: secretKey,
AdminUser: adminUser,
AdminPass: adminPass,
Logger: logger, // handler (application level logger)
TestServiceOptions: services.TestServiceOptions{
TestsLogsPath: testsLogsPath, // directory to store logs of each test
Logger: logger,
},
}, nil
}

func handleShutdown(apiServer *api.API, db *gorm.DB, logger *logrus.Logger) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)

sqlDB, err := db.DB()
if err != nil {
logger.Errorf("failed to get sql db: %v", err)
}

go func() {
sig := <-quit
logger.Infof("Received signal: %v. Shutting down gracefully...", sig)
if err := sqlDB.Close(); err != nil {
logger.Errorf("failed to close sql db: %v", err)
}
logger.Info("DB connection closed")
if err := apiServer.Stop(context.Background()); err != nil {
logger.Errorf("failed to stop api server: %v", err)
}
logger.Info("API server stopped")
os.Exit(0)
}()
}
13 changes: 13 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

import (
"github.com/sirupsen/logrus"

"github.com/celestiaorg/knuu/cmd/root"
)

func main() {
if err := root.Execute(); err != nil {
logrus.WithError(err).Fatal("failed to execute command")
}
}
22 changes: 22 additions & 0 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
@@ -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())
}
Loading
Loading