Codevault initial commit
This commit is contained in:
83
cmd/add_user.go
Normal file
83
cmd/add_user.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/kanootoko/codevault/internal/runcmd"
|
||||
)
|
||||
|
||||
var addUserCmd = &cobra.Command{
|
||||
Use: "add-user",
|
||||
Short: "Add or update a user in the database",
|
||||
Long: `Add or update a user in the database. Does not read the application config -
|
||||
only a database path is needed; if the database file does not exist it is
|
||||
created together with its schema.`,
|
||||
RunE: runAddUser,
|
||||
}
|
||||
|
||||
func init() {
|
||||
addUserCmd.Flags().String("db-path", "./codevault.db", "Path to the SQLite database file")
|
||||
addUserCmd.Flags().String("login", "", "Login of the user to add or update")
|
||||
addUserCmd.Flags().String("password", "", "Password for the user (if omitted, prompted interactively; "+
|
||||
"passing it as a flag can leak the password via shell history)")
|
||||
_ = addUserCmd.MarkFlagRequired("login")
|
||||
|
||||
rootCmd.AddCommand(addUserCmd)
|
||||
}
|
||||
|
||||
func runAddUser(cmd *cobra.Command, args []string) error {
|
||||
dbPath, err := cmd.Flags().GetString("db-path")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on getting db-path option: %w", err)
|
||||
}
|
||||
login, err := cmd.Flags().GetString("login")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on getting login option: %w", err)
|
||||
}
|
||||
password, err := cmd.Flags().GetString("password")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on getting password option: %w", err)
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
password, err = readPasswordWithConfirmation(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on reading password: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := runcmd.AddUser(dbPath, login, password); err != nil {
|
||||
return fmt.Errorf("error on adding user: %w", err)
|
||||
}
|
||||
|
||||
cmd.Println("user saved successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPasswordWithConfirmation(cmd *cobra.Command) (string, error) {
|
||||
fd := int(os.Stdin.Fd())
|
||||
|
||||
cmd.Print("Password: ")
|
||||
password, err := term.ReadPassword(fd)
|
||||
cmd.Println()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error on reading password: %w", err)
|
||||
}
|
||||
|
||||
cmd.Print("Confirm password: ")
|
||||
confirmation, err := term.ReadPassword(fd)
|
||||
cmd.Println()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error on reading password confirmation: %w", err)
|
||||
}
|
||||
|
||||
if string(password) != string(confirmation) {
|
||||
return "", fmt.Errorf("passwords do not match")
|
||||
}
|
||||
|
||||
return string(password), nil
|
||||
}
|
||||
31
cmd/config_example.go
Normal file
31
cmd/config_example.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kanootoko/codevault/internal/runcmd"
|
||||
)
|
||||
|
||||
var configExampleCmd = &cobra.Command{
|
||||
Use: "config-example <config-path>",
|
||||
Short: "Get server config example",
|
||||
Long: `Generate and save to file a sample config for further manual editing.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
configPath := args[0]
|
||||
|
||||
err := runcmd.ConfigExample(configPath)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("error on saving example config: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configExampleCmd)
|
||||
}
|
||||
23
cmd/root.go
Normal file
23
cmd/root.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kanootoko/codevault/internal"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "codevault",
|
||||
Short: "CodeVault - store and retrieve secrets by access code",
|
||||
Long: `CodeVault is a small HTTP service that stores string values under access codes.`,
|
||||
SilenceUsage: true,
|
||||
Version: internal.Version,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
123
cmd/run.go
Normal file
123
cmd/run.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/kanootoko/codevault/internal/config"
|
||||
"github.com/kanootoko/codevault/internal/observability/logging"
|
||||
"github.com/kanootoko/codevault/internal/observability/metrics"
|
||||
"github.com/kanootoko/codevault/internal/runcmd"
|
||||
"github.com/kanootoko/codevault/internal/utils"
|
||||
"github.com/kanootoko/codevault/internal/utils/gshutdown"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Start the CodeVault server with the given config",
|
||||
Long: `Start the CodeVault HTTP server using the given config file.`,
|
||||
PreRunE: preRunE,
|
||||
RunE: runE,
|
||||
}
|
||||
|
||||
func init() {
|
||||
_ = runCmd.Flags().StringP("config", "c", "", "Path to config [CONFIG envvar]")
|
||||
utils.TryFlagFromEnv(runCmd, "config", "CONFIG", true)
|
||||
|
||||
rootCmd.AddCommand(runCmd)
|
||||
}
|
||||
|
||||
func preRunE(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
ctx, cancelCtx = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
configPath, _ := cmd.Flags().GetString("config")
|
||||
if configPath == "" {
|
||||
return nil // will fail on RunE() with a proper error
|
||||
}
|
||||
|
||||
appConfig, err := config.ReadConfig(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on reading config: %w", err)
|
||||
}
|
||||
if err = appConfig.Validate(); err != nil {
|
||||
return fmt.Errorf("error on config validation: %w", err)
|
||||
}
|
||||
|
||||
ctx, err = configureLoggerCtx(ctx, appConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to configure logger: %w", err)
|
||||
}
|
||||
logger := logging.LoggerMustFromContext(ctx)
|
||||
ctx = gshutdown.CtxWithGracefulShutdown(ctx, logger.Named("graceful_shutdown"))
|
||||
|
||||
if err = configureObservability(ctx, appConfig.Observability); err != nil {
|
||||
return fmt.Errorf("failed to configure observability: %w", err)
|
||||
}
|
||||
cmd.SetContext(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runE(cmd *cobra.Command, args []string) error {
|
||||
configPath, err := cmd.Flags().GetString("config")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on getting config option: %w", err)
|
||||
}
|
||||
|
||||
appConfig, err := config.ReadConfig(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error on reading config: %w", err)
|
||||
}
|
||||
|
||||
defer postRun(cmd.Context())
|
||||
|
||||
err = runcmd.RunServer(cmd.Context(), appConfig)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("error on running server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func postRun(ctx context.Context) {
|
||||
logger := logging.LoggerMustFromContext(ctx)
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("uncaught panic", zap.Any("panic data", err), zap.Stack("stack_trace"))
|
||||
defer panic(err)
|
||||
}
|
||||
cancelCtx()
|
||||
err := gshutdown.AwaitWithTimeout(ctx, time.Second*10)
|
||||
if err != nil {
|
||||
logger.Error("graceful shutdown failed", zap.Error(err))
|
||||
}
|
||||
loggerCloseFunc()
|
||||
}
|
||||
|
||||
func configureLoggerCtx(ctx context.Context, appConfig config.Config) (context.Context, error) {
|
||||
logger, closeFunc, err := logging.ConfigureLogger(ctx, appConfig.Observability.Logging, appConfig.Environment == config.ProductionEnvironment)
|
||||
zap.ReplaceGlobals(logger)
|
||||
loggerCloseFunc = closeFunc
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("error on logging initialization: %w", err)
|
||||
}
|
||||
return logging.CtxWithLogger(ctx, logger), nil
|
||||
}
|
||||
|
||||
func configureObservability(ctx context.Context, cfg config.ObservabilityConfig) error {
|
||||
if cfg.Metrics.Host != "" {
|
||||
if err := metrics.Configure(ctx, cfg.Metrics); err != nil {
|
||||
return fmt.Errorf("error on starting Prometheus metrics handler: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var loggerCloseFunc func() // so the logger is closed after all other context-wide subtasks
|
||||
var cancelCtx context.CancelFunc
|
||||
Reference in New Issue
Block a user