(Feat): Initial Commit
This commit is contained in:
11
cmd/README.md
Normal file
11
cmd/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# `/cmd`
|
||||
|
||||
Main applications for this project.
|
||||
|
||||
Don't put a lot of code in the application directory. If you think the code can be imported and used in other projects, then it should live in the `/pkg` directory. If the code is not reusable or if you don't want others to reuse it, put that code in the `/internal` directory. You'll be surprised what others will do, so be explicit about your intentions!
|
||||
|
||||
We manage our applications via cobra ([`cobra-cli`](https://github.com/spf13/cobra-cli) is installed within the `Dockerfile` within the development stage, `/cmd` consumes the core [`cobra`](https://github.com/spf13/cobra) library), see:
|
||||
* https://github.com/spf13/cobra#getting-started
|
||||
* https://github.com/spf13/cobra-cli/blob/main/README.md#add-commands-to-a-project
|
||||
|
||||
Also see https://github.com/golang-standards/project-layout/tree/master/cmd
|
||||
13
cmd/db/db.go
Normal file
13
cmd/db/db.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func New() *cobra.Command {
|
||||
return command.NewSubcommandGroup("db",
|
||||
newMigrate(),
|
||||
newSeed(),
|
||||
)
|
||||
}
|
||||
97
cmd/db/migrate.go
Normal file
97
cmd/db/migrate.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||
"github.com/rs/zerolog/log"
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newMigrate() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "Executes all migrations which are not yet applied.",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
migrateCmdFunc()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateCmdFunc() {
|
||||
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
n, err := ApplyMigrations(ctx, s.Config)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error while applying migrations")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Int("appliedMigrationsCount", n).Msg("Successfully applied migrations")
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to apply migrations")
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyMigrations(ctx context.Context, serviceConfig config.Server) (int, error) {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
// pin migrate to use the globally defined `migrations` table identifier
|
||||
migrate.SetTable(config.DatabaseMigrationTable)
|
||||
|
||||
db, err := sql.Open("postgres", serviceConfig.Database.ConnectionString())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to open the database: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return 0, fmt.Errorf("failed to ping the database: %w", err)
|
||||
}
|
||||
|
||||
// In case an old default sql-migrate migration table (named "gorp_migrations") still exists we rename it to the new name equivalent
|
||||
// in sync with the settings in dbconfig.yml and config.DatabaseMigrationTable.
|
||||
if _, err := db.ExecContext(ctx, fmt.Sprintf("ALTER TABLE IF EXISTS gorp_migrations RENAME TO %s;", config.DatabaseMigrationTable)); err != nil {
|
||||
return 0, fmt.Errorf("failed to rename migrations table: %w", err)
|
||||
}
|
||||
|
||||
migrations := &migrate.FileMigrationSource{
|
||||
Dir: config.DatabaseMigrationFolder,
|
||||
}
|
||||
|
||||
missingMigrations, _, err := migrate.PlanMigration(db, "postgres", migrations, migrate.Up, 0)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error while planning migrations")
|
||||
return 0, fmt.Errorf("failed to plan migrations: %w", err)
|
||||
}
|
||||
|
||||
var appliedMigrationsCount int
|
||||
for i := 0; i < len(missingMigrations); i++ {
|
||||
log.Info().Str("migrationId", missingMigrations[i].Id).Msg("Applying migration")
|
||||
|
||||
n, err := migrate.ExecMax(db, "postgres", migrations, migrate.Up, 1)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error while applying migration")
|
||||
return 0, fmt.Errorf("failed to apply migration: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Int("appliedMigrationsCount", n).Msg("Applied migration")
|
||||
|
||||
appliedMigrationsCount += n
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return appliedMigrationsCount, nil
|
||||
}
|
||||
77
cmd/db/seed.go
Normal file
77
cmd/db/seed.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
data "allaboutapps.dev/aw/go-starter/internal/data/fixtures"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||
dbutil "allaboutapps.dev/aw/go-starter/internal/util/db"
|
||||
"github.com/aarondl/sqlboiler/v4/boil"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newSeed() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "seed",
|
||||
Short: "Inserts or updates fixtures to the database.",
|
||||
Long: `Uses upsert to add test data to the current environment.`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
seedCmdFunc()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func seedCmdFunc() {
|
||||
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
err := ApplySeedFixtures(ctx, s.Config)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error while applying seed fixtures")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Msg("Successfully applied seed fixtures")
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to apply migrations")
|
||||
}
|
||||
}
|
||||
|
||||
func ApplySeedFixtures(ctx context.Context, config config.Server) error {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
db, err := sql.Open("postgres", config.Database.ConnectionString())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open the database: %w", err)
|
||||
}
|
||||
|
||||
defer db.Close()
|
||||
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("failed to ping the database: %w", err)
|
||||
}
|
||||
|
||||
// insert fixtures in an auto-managed db transaction
|
||||
return dbutil.WithTransaction(ctx, db, func(tx boil.ContextExecutor) error {
|
||||
fixtures := data.Upserts()
|
||||
|
||||
for _, fixture := range fixtures {
|
||||
if err := fixture.Upsert(ctx, tx, true, nil, boil.Infer(), boil.Infer()); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to upsert fixture")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Int("fixturesCount", len(fixtures)).Msg("Successfully upserted fixtures")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
38
cmd/env/env.go
vendored
Normal file
38
cmd/env/env.go
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func New() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Prints the env",
|
||||
Long: `Prints the currently applied env
|
||||
|
||||
You may use this cmd to get an overview about how
|
||||
your ENV_VARS are bound by the server config.
|
||||
Please note that certain secrets are automatically
|
||||
removed from this output.`,
|
||||
Run: func(_ *cobra.Command /* cmd */, _ []string /* args */) {
|
||||
runEnv()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runEnv() {
|
||||
config := config.DefaultServiceConfigFromEnv()
|
||||
|
||||
result, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to marshal the env")
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
fmt.Println(string(result))
|
||||
}
|
||||
89
cmd/probe/liveness.go
Normal file
89
cmd/probe/liveness.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/handlers/common"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type LivenessFlags struct {
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func newLiveness() *cobra.Command {
|
||||
var flags LivenessFlags
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "liveness",
|
||||
Short: "Runs liveness probes",
|
||||
Long: `Runs connection livenesss probes
|
||||
|
||||
This command triggers the same livenesss probes as in
|
||||
/-/healthy (apart from the actual server.ready
|
||||
probe) and prints the results to stdout. Fails with
|
||||
non zero exitcode on encountered errors.
|
||||
|
||||
A typical usecase of this command are liveness probes
|
||||
to take action if dependant services (e.g. DB, NFS
|
||||
mounts) become unstable. You may also use this to
|
||||
ensure all requirements are fulfilled before starting
|
||||
the app server.`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
livenessCmdFunc(flags)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&flags.Verbose, verboseFlag, "v", false, "Show verbose output.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func livenessCmdFunc(flags LivenessFlags) {
|
||||
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
errs, err := runLiveness(ctx, s.Config, flags)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to run liveness probes")
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
log.Fatal().Errs("errs", errs).Msg("Unhealthy.")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to run liveness probes")
|
||||
}
|
||||
}
|
||||
|
||||
func runLiveness(ctx context.Context, config config.Server, flags LivenessFlags) ([]error, error) {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
db, err := sql.Open("postgres", config.Database.ConnectionString())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to open the database")
|
||||
return nil, fmt.Errorf("failed to open the database: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
livenessCtx, cancel := context.WithTimeout(context.Background(), config.Management.LivenessTimeout)
|
||||
defer cancel()
|
||||
|
||||
str, errs := common.ProbeLiveness(livenessCtx, db, config.Management.ProbeWriteablePathsAbs, config.Management.ProbeWriteableTouchfile)
|
||||
|
||||
if flags.Verbose {
|
||||
log.Info().Msg(str)
|
||||
}
|
||||
|
||||
return errs, nil
|
||||
}
|
||||
17
cmd/probe/probe.go
Normal file
17
cmd/probe/probe.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
verboseFlag string = "verbose"
|
||||
)
|
||||
|
||||
func New() *cobra.Command {
|
||||
return command.NewSubcommandGroup("probe",
|
||||
newLiveness(),
|
||||
newReadiness(),
|
||||
)
|
||||
}
|
||||
89
cmd/probe/readiness.go
Normal file
89
cmd/probe/readiness.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/handlers/common"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ReadinessFlags struct {
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func newReadiness() *cobra.Command {
|
||||
var flags ReadinessFlags
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "readiness",
|
||||
Short: "Runs readiness probes",
|
||||
Long: `Runs connection readinesss probes
|
||||
|
||||
This command triggers the same readinesss probes as in
|
||||
/-/ready (apart from the actual server.ready
|
||||
probe) and prints the results to stdout. Fails with
|
||||
non zero exitcode on encountered errors.
|
||||
|
||||
A typical usecase of this command are readiness probes
|
||||
to take action if dependant services (e.g. DB, NFS
|
||||
mounts) become unstable. You may also use this to
|
||||
ensure all requirements are fulfilled before starting
|
||||
the app server.`,
|
||||
Run: func(_ *cobra.Command, _ []string /* args */) {
|
||||
readinessCmdFunc(flags)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&flags.Verbose, verboseFlag, "v", false, "Show verbose output.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func readinessCmdFunc(flags ReadinessFlags) {
|
||||
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
errs, err := RunReadiness(ctx, s.Config, flags)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to run readiness probes")
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
log.Fatal().Errs("errs", errs).Msg("Unhealthy.")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to run readiness probes")
|
||||
}
|
||||
}
|
||||
|
||||
func RunReadiness(ctx context.Context, config config.Server, flags ReadinessFlags) ([]error, error) {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
db, err := sql.Open("postgres", config.Database.ConnectionString())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to open the database")
|
||||
return nil, fmt.Errorf("failed to open the database: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
readinessCtx, cancel := context.WithTimeout(context.Background(), config.Management.ReadinessTimeout)
|
||||
defer cancel()
|
||||
|
||||
str, errs := common.ProbeReadiness(readinessCtx, db, config.Management.ProbeWriteablePathsAbs)
|
||||
|
||||
if flags.Verbose {
|
||||
log.Info().Msg(str)
|
||||
}
|
||||
|
||||
return errs, nil
|
||||
}
|
||||
44
cmd/root.go
Normal file
44
cmd/root.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/cmd/db"
|
||||
"allaboutapps.dev/aw/go-starter/cmd/env"
|
||||
"allaboutapps.dev/aw/go-starter/cmd/probe"
|
||||
"allaboutapps.dev/aw/go-starter/cmd/server"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Version: config.GetFormattedBuildArgs(),
|
||||
Use: "app",
|
||||
Short: config.ModuleName,
|
||||
Long: fmt.Sprintf(`%v
|
||||
|
||||
A stateless RESTful JSON service written in Go.
|
||||
Requires configuration through ENV.`, config.ModuleName),
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
rootCmd.SetVersionTemplate(`{{printf "%s\n" .Version}}`)
|
||||
|
||||
// attach the subcommands
|
||||
rootCmd.AddCommand(
|
||||
db.New(),
|
||||
env.New(),
|
||||
probe.New(),
|
||||
server.New(),
|
||||
)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to execute root command")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
105
cmd/server/server.go
Normal file
105
cmd/server/server.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"allaboutapps.dev/aw/go-starter/cmd/db"
|
||||
"allaboutapps.dev/aw/go-starter/cmd/probe"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api"
|
||||
"allaboutapps.dev/aw/go-starter/internal/api/router"
|
||||
"allaboutapps.dev/aw/go-starter/internal/config"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util"
|
||||
"allaboutapps.dev/aw/go-starter/internal/util/command"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Flags struct {
|
||||
ProbeReadiness bool
|
||||
ApplyMigrations bool
|
||||
SeedFixtures bool
|
||||
}
|
||||
|
||||
func New() *cobra.Command {
|
||||
var flags Flags
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Starts the server",
|
||||
Long: `Starts the stateless RESTful JSON server
|
||||
|
||||
Requires configuration through ENV and
|
||||
a fully migrated PostgreSQL database.`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
runServer(flags)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&flags.ProbeReadiness, "probe", "p", false, "Probe readiness before startup.")
|
||||
cmd.Flags().BoolVarP(&flags.ApplyMigrations, "migrate", "m", false, "Apply migrations before startup.")
|
||||
cmd.Flags().BoolVarP(&flags.SeedFixtures, "seed", "s", false, "Seed fixtures into database before startup.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runServer(flags Flags) {
|
||||
err := command.WithServer(context.Background(), config.DefaultServiceConfigFromEnv(), func(ctx context.Context, s *api.Server) error {
|
||||
log := util.LogFromContext(ctx)
|
||||
|
||||
if flags.ProbeReadiness {
|
||||
errs, err := probe.RunReadiness(ctx, s.Config, probe.ReadinessFlags{
|
||||
Verbose: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to run readiness probes")
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
log.Fatal().Errs("errs", errs).Msg("Unhealthy.")
|
||||
}
|
||||
}
|
||||
|
||||
if flags.ApplyMigrations {
|
||||
_, err := db.ApplyMigrations(ctx, s.Config)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error while applying migrations")
|
||||
}
|
||||
}
|
||||
|
||||
if flags.SeedFixtures {
|
||||
err := db.ApplySeedFixtures(ctx, s.Config)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error while applying seed fixtures")
|
||||
}
|
||||
}
|
||||
|
||||
err := router.Init(s)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to initialize router")
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := s.Start(); err != nil {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
log.Info().Msg("Server closed")
|
||||
} else {
|
||||
log.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user