diff --git a/go.mod b/go.mod index 4e9a604..68be556 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/swaggo/swag v1.16.6 github.com/uptrace/bun v1.2.18 github.com/uptrace/bun/dialect/mysqldialect v1.2.18 + github.com/urfave/cli/v3 v3.8.0 go.uber.org/fx v1.24.0 go.uber.org/zap v1.27.1 golang.org/x/crypto v0.50.0 diff --git a/go.sum b/go.sum index c3b5bf0..fc5b428 100644 --- a/go.sum +++ b/go.sum @@ -196,6 +196,8 @@ github.com/uptrace/bun v1.2.18 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU= github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y= github.com/uptrace/bun/dialect/mysqldialect v1.2.18 h1:w+3iuWa4cVmsXXt8w28A0+Ikve77AU0tiBWG6UvGvM8= github.com/uptrace/bun/dialect/mysqldialect v1.2.18/go.mod h1:FhJEK620SM9HJ9fx0/IHT7k1cpn2+6MmtKvNptWezPY= +github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= +github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= diff --git a/internal/app.go b/internal/app.go index 6f57097..2a8d506 100644 --- a/internal/app.go +++ b/internal/app.go @@ -2,78 +2,49 @@ package internal import ( "context" + "fmt" + "os" + "os/signal" + "syscall" - "github.com/bit-issues/backend/internal/attachments" - "github.com/bit-issues/backend/internal/comments" - "github.com/bit-issues/backend/internal/config" - "github.com/bit-issues/backend/internal/db" - "github.com/bit-issues/backend/internal/jwt" - "github.com/bit-issues/backend/internal/projects" - "github.com/bit-issues/backend/internal/server" - "github.com/bit-issues/backend/internal/storage" - "github.com/bit-issues/backend/internal/tasks" - "github.com/bit-issues/backend/internal/users" - "github.com/bit-issues/backend/pkg/miniofx" - "github.com/go-core-fx/bunfx" - "github.com/go-core-fx/fiberfx" - "github.com/go-core-fx/goosefx" + "github.com/bit-issues/backend/internal/commands" "github.com/go-core-fx/healthfx" - "github.com/go-core-fx/logger" - "github.com/go-core-fx/sqlfx" - "github.com/go-core-fx/validatorfx" - "go.uber.org/fx" - "go.uber.org/zap" + "github.com/samber/lo" + "github.com/urfave/cli/v3" ) func Run(version healthfx.Version) { - fx.New( - // CORE MODULES - logger.Module(), - logger.WithFxDefaultLogger(), - // badgerfx.Module(), - bunfx.Module(), - // cachefx.Module(), - fiberfx.Module(), - // gocqlfx.Module(), - // gocqlxfx.Module(), - sqlfx.Module(), - goosefx.Module(), - // gormfx.Module(), - healthfx.Module(), - // openrouterfx.Module(), - // redisfx.Module(), - // sqlxfx.Module(), - // telegofx.Module(true), - validatorfx.Module(), - // watermillfx.Module(), - miniofx.Module(), - // - // APP MODULES - config.Module(), - db.Module(), - server.Module(), - storage.Module(), - // - // BUSINESS MODULES - fx.Supply(version), - jwt.Module(), - users.Module(), - projects.Module(), - tasks.Module(), - attachments.Module(), - comments.Module(), - // - fx.Invoke(func(lc fx.Lifecycle, logger *zap.Logger) { - lc.Append(fx.Hook{ - OnStart: func(_ context.Context) error { - logger.Info("app started") + app := &cli.Command{ + Name: "backend", + Usage: "BitIssues Backend", + Description: `BitIssues Backend`, + Version: version.Version, + DefaultCommand: "serve", + Flags: []cli.Flag{}, + Commands: []*cli.Command{ + { + Name: "serve", + Usage: "Start the HTTP server", + Description: `Start the HTTP server`, + Action: func(ctx context.Context, _ *cli.Command) error { + if err := commands.Serve(ctx, version); err != nil { + return fmt.Errorf("serve: %w", err) + } return nil }, - OnStop: func(_ context.Context) error { - logger.Info("app stopped") - return nil - }, - }) - }), - ).Run() + }, + }, + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + + if err := app.Run(ctx, os.Args); err != nil { + exitCode := 1 + if exitErr, ok := lo.ErrorsAs[cli.ExitCoder](err); ok { + exitCode = exitErr.ExitCode() + } + stop() + os.Exit(exitCode) + } + stop() } diff --git a/internal/commands/serve.go b/internal/commands/serve.go new file mode 100644 index 0000000..4aaca28 --- /dev/null +++ b/internal/commands/serve.go @@ -0,0 +1,100 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/bit-issues/backend/internal/attachments" + "github.com/bit-issues/backend/internal/comments" + "github.com/bit-issues/backend/internal/config" + "github.com/bit-issues/backend/internal/db" + "github.com/bit-issues/backend/internal/jwt" + "github.com/bit-issues/backend/internal/projects" + "github.com/bit-issues/backend/internal/server" + "github.com/bit-issues/backend/internal/storage" + "github.com/bit-issues/backend/internal/tasks" + "github.com/bit-issues/backend/internal/users" + "github.com/bit-issues/backend/pkg/miniofx" + "github.com/go-core-fx/bunfx" + "github.com/go-core-fx/fiberfx" + "github.com/go-core-fx/goosefx" + "github.com/go-core-fx/healthfx" + "github.com/go-core-fx/logger" + "github.com/go-core-fx/sqlfx" + "github.com/go-core-fx/validatorfx" + "go.uber.org/fx" + "go.uber.org/zap" +) + +func Serve(ctx context.Context, version healthfx.Version) error { + app := fx.New( + // CORE MODULES + logger.Module(), + logger.WithFxDefaultLogger(), + // badgerfx.Module(), + bunfx.Module(), + // cachefx.Module(), + fiberfx.Module(), + // gocqlfx.Module(), + // gocqlxfx.Module(), + sqlfx.Module(), + goosefx.Module(), + // gormfx.Module(), + healthfx.Module(), + // openrouterfx.Module(), + // redisfx.Module(), + // sqlxfx.Module(), + // telegofx.Module(true), + validatorfx.Module(), + // watermillfx.Module(), + miniofx.Module(), + // + // APP MODULES + config.Module(), + db.Module(), + server.Module(), + storage.Module(), + // + // BUSINESS MODULES + fx.Supply(version), + jwt.Module(), + users.Module(), + projects.Module(), + tasks.Module(), + attachments.Module(), + comments.Module(), + // + fx.Invoke(func(lc fx.Lifecycle, logger *zap.Logger) { + lc.Append(fx.Hook{ + OnStart: func(_ context.Context) error { + logger.Info("app started") + return nil + }, + OnStop: func(_ context.Context) error { + logger.Info("app stopped") + return nil + }, + }) + }), + ) + + startCtx, cancelStart := context.WithTimeout(ctx, app.StartTimeout()) + defer cancelStart() + if err := app.Start(startCtx); err != nil { + return fmt.Errorf("failed to start app: %w", err) + } + + select { + case <-ctx.Done(): + case <-app.Done(): + } + + stopCtx, cancelStop := context.WithTimeout(context.Background(), app.StopTimeout()) + defer cancelStop() + + if err := app.Stop(stopCtx); err != nil { + return fmt.Errorf("failed to stop app: %w", err) + } + + return nil +}