From bb7c4bcedc3e42b3c9b5e70d28a7cf17e1ac245e Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Wed, 29 Jan 2025 21:57:34 +0000 Subject: [PATCH] Simplify serving and setup in main binary --- cmd/orchid/main.go | 117 ++++++++++++++++++++++++++++++++++++++++---- cmd/orchid/serve.go | 99 ------------------------------------- cmd/orchid/setup.go | 83 +++++++++++-------------------- go.mod | 1 - go.sum | 2 - 5 files changed, 136 insertions(+), 166 deletions(-) delete mode 100644 cmd/orchid/serve.go diff --git a/cmd/orchid/main.go b/cmd/orchid/main.go index 1a149dc..79757a0 100644 --- a/cmd/orchid/main.go +++ b/cmd/orchid/main.go @@ -1,20 +1,117 @@ package main import ( - "context" + "errors" "flag" - "github.com/google/subcommands" + "github.com/1f349/mjwt" + "github.com/1f349/orchid" + httpAcme "github.com/1f349/orchid/http-acme" + "github.com/1f349/orchid/logger" + "github.com/1f349/orchid/renewal" + "github.com/1f349/orchid/servers" + "github.com/1f349/violet/utils" + _ "github.com/mattn/go-sqlite3" + exitReload "github.com/mrmelon54/exit-reload" + "gopkg.in/yaml.v3" "os" + "path/filepath" + "sync" ) -func main() { - subcommands.Register(subcommands.HelpCommand(), "") - subcommands.Register(subcommands.FlagsCommand(), "") - subcommands.Register(subcommands.CommandsCommand(), "") - subcommands.Register(&serveCmd{}, "") - subcommands.Register(&setupCmd{}, "") +var configPath string +func main() { + flag.StringVar(&configPath, "conf", "", "/path/to/config.json : path to the config file") flag.Parse() - ctx := context.Background() - os.Exit(int(subcommands.Execute(ctx))) + + logger.Logger.Info("Starting...") + + if configPath == "" { + logger.Logger.Fatal("Config flag is missing") + } + + wd, err := getWD(configPath) + if err != nil { + logger.Logger.Fatal("Failed to find config directory: ", "err", err) + } + + // try to open the config file + openConf, err := os.Open(configPath) + switch { + case err == nil: + break + case os.IsNotExist(err): + // handle potential errors during setup + err = trySetup(wd) + switch { + case errors.Is(err, errExitSetup): + // exit setup without questions + return + case err == nil: + return + default: + logger.Logger.Fatal("Failed to run setup", "err", err) + } + default: + logger.Logger.Fatal("Open config file: ", "err", err) + } + + // config file opened with no errors + + defer openConf.Close() + + var config startUpConfig + err = yaml.NewDecoder(openConf).Decode(&config) + if err != nil { + logger.Logger.Fatal("Invalid config file: ", "err", err) + } + + runDaemon(wd, config) +} + +func runDaemon(wd string, conf startUpConfig) { + // load the MJWT RSA public key from a pem encoded file + mJwtVerify, err := mjwt.NewKeyStoreFromPath(filepath.Join(wd, "keys")) + if err != nil { + logger.Logger.Fatal("Failed to load MJWT verifier public key from file", "path", filepath.Join(wd, "keys"), "err", err) + } + + // open sqlite database + db, err := orchid.InitDB(filepath.Join(wd, "orchid.db.sqlite")) + if err != nil { + logger.Logger.Fatal("Failed to open database", "err", err) + } + + certDir := filepath.Join(wd, "renewal-certs") + keyDir := filepath.Join(wd, "renewal-keys") + + wg := &sync.WaitGroup{} + acmeProv, err := httpAcme.NewHttpAcmeProvider(filepath.Join(wd, "tokens.yml"), conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl) + if err != nil { + logger.Logger.Fatal("HTTP Acme Error", "err", err) + } + renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir) + if err != nil { + logger.Logger.Fatal("Service Error", "err", err) + } + srv := servers.NewApiServer(conf.Listen, db, mJwtVerify, conf.Domains) + logger.Logger.Info("Starting API server", "listen", srv.Addr) + go utils.RunBackgroundHttp(logger.Logger, srv) + + exitReload.ExitReload("Violet", func() {}, func() { + // stop renewal service and api server + renewalService.Shutdown() + srv.Close() + }) +} + +func getWD(configPath string) (string, error) { + if configPath == "" { + return os.Getwd() + } + wdAbs, err := filepath.Abs(configPath) + if err != nil { + return "", err + } + return filepath.Dir(wdAbs), nil } diff --git a/cmd/orchid/serve.go b/cmd/orchid/serve.go deleted file mode 100644 index 007b7c1..0000000 --- a/cmd/orchid/serve.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "context" - "flag" - "github.com/1f349/mjwt" - "github.com/1f349/orchid" - httpAcme "github.com/1f349/orchid/http-acme" - "github.com/1f349/orchid/logger" - "github.com/1f349/orchid/renewal" - "github.com/1f349/orchid/servers" - "github.com/1f349/violet/utils" - "github.com/google/subcommands" - _ "github.com/mattn/go-sqlite3" - "github.com/mrmelon54/exit-reload" - "gopkg.in/yaml.v3" - "os" - "path/filepath" - "sync" -) - -type serveCmd struct{ configPath string } - -func (s *serveCmd) Name() string { return "serve" } -func (s *serveCmd) Synopsis() string { return "Serve certificate renewal service" } -func (s *serveCmd) SetFlags(f *flag.FlagSet) { - f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file") -} -func (s *serveCmd) Usage() string { - return `serve [-conf ] - Serve certificate renewal service using information from config file -` -} - -func (s *serveCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { - logger.Logger.Info("Starting...") - - if s.configPath == "" { - logger.Logger.Error("Config flag is missing") - return subcommands.ExitUsageError - } - - openConf, err := os.Open(s.configPath) - if err != nil { - if os.IsNotExist(err) { - logger.Logger.Error("Missing config file") - } else { - logger.Logger.Error("Open config file: ", "err", err) - } - return subcommands.ExitFailure - } - - var conf startUpConfig - err = yaml.NewDecoder(openConf).Decode(&conf) - if err != nil { - logger.Logger.Error("Invalid config file: ", "err", err) - return subcommands.ExitFailure - } - - wd := filepath.Dir(s.configPath) - normalLoad(conf, wd) - return subcommands.ExitSuccess -} - -func normalLoad(conf startUpConfig, wd string) { - // load the MJWT RSA public key from a pem encoded file - mJwtVerify, err := mjwt.NewKeyStoreFromPath(filepath.Join(wd, "keys")) - if err != nil { - logger.Logger.Fatal("Failed to load MJWT verifier public key from file", "path", filepath.Join(wd, "keys"), "err", err) - } - - // open sqlite database - db, err := orchid.InitDB(filepath.Join(wd, "orchid.db.sqlite")) - if err != nil { - logger.Logger.Fatal("Failed to open database", "err", err) - } - - certDir := filepath.Join(wd, "renewal-certs") - keyDir := filepath.Join(wd, "renewal-keys") - - wg := &sync.WaitGroup{} - acmeProv, err := httpAcme.NewHttpAcmeProvider(filepath.Join(wd, "tokens.yml"), conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl) - if err != nil { - logger.Logger.Fatal("HTTP Acme Error", "err", err) - } - renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir) - if err != nil { - logger.Logger.Fatal("Service Error", "err", err) - } - srv := servers.NewApiServer(conf.Listen, db, mJwtVerify, conf.Domains) - logger.Logger.Info("Starting API server", "listen", srv.Addr) - go utils.RunBackgroundHttp(logger.Logger, srv) - - exit_reload.ExitReload("Violet", func() {}, func() { - // stop renewal service and api server - renewalService.Shutdown() - srv.Close() - }) -} diff --git a/cmd/orchid/setup.go b/cmd/orchid/setup.go index 956d876..e63f981 100644 --- a/cmd/orchid/setup.go +++ b/cmd/orchid/setup.go @@ -2,16 +2,15 @@ package main import ( "bytes" - "context" "crypto/rsa" "crypto/x509" "encoding/pem" - "flag" + "errors" "fmt" httpAcme "github.com/1f349/orchid/http-acme" + "github.com/1f349/orchid/logger" "github.com/1f349/orchid/renewal" "github.com/AlecAivazis/survey/v2" - "github.com/google/subcommands" "gopkg.in/yaml.v3" "math/rand" "net" @@ -22,37 +21,18 @@ import ( "time" ) -type setupCmd struct{ wdPath string } - -func (s *setupCmd) Name() string { return "setup" } -func (s *setupCmd) Synopsis() string { return "Setup certificate renewal service" } -func (s *setupCmd) SetFlags(f *flag.FlagSet) { - f.StringVar(&s.wdPath, "wd", ".", "Path to the directory to create config files in (defaults to the working directory)") -} -func (s *setupCmd) Usage() string { - return `setup [-wd ] - Setup Orchid automatically by answering questions. -` -} - -func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { - // get absolute path to specify files - wdAbs, err := filepath.Abs(s.wdPath) - if err != nil { - fmt.Println("[Orchid] Failed to get full directory path: ", err) - return subcommands.ExitFailure - } +var errExitSetup = errors.New("exit setup") +func trySetup(wd string) error { // ask about running the setup steps createFile := false - err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Orchid config files in this directory: '%s'?", wdAbs)}, &createFile) + err := survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Orchid config files in this directory: '%s'?", wd)}, &createFile) if err != nil { - fmt.Println("[Orchid] Error: ", err) - return subcommands.ExitFailure + return err } if !createFile { - fmt.Println("[Orchid] Goodbye") - return subcommands.ExitSuccess + logger.Logger.Info("Goodbye") + return errExitSetup } var answers struct { @@ -64,7 +44,6 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa AcmeRefreshUrl string LEEmail string } - _ = answers // ask main questions err = survey.Ask([]*survey.Question{ @@ -88,8 +67,7 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa }, }, &answers) if err != nil { - fmt.Println("[Orchid] Error: ", err) - return subcommands.ExitFailure + return err } if answers.AcmeRefresh != "" { @@ -111,35 +89,31 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa }, }, &answers) if err != nil { - fmt.Println("[Orchid] Error: ", err) - return subcommands.ExitFailure + return err } } key, err := rsa.GenerateKey(rand.New(rand.NewSource(time.Now().UnixNano())), 4096) if err != nil { - fmt.Println("[Orchid] Error: ", err) - return subcommands.ExitFailure + return fmt.Errorf("failed to generate private key: %w", err) } keyBytes := x509.MarshalPKCS1PrivateKey(key) keyBuf := new(bytes.Buffer) err = pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes}) if err != nil { - fmt.Println("[Orchid] Error: ", err) - return subcommands.ExitFailure + return fmt.Errorf("failed to PEM encode private key: %w", err) } // write config file - confFile := filepath.Join(wdAbs, "config.yml") + confFile := filepath.Join(wd, "config.yml") createConf, err := os.Create(confFile) if err != nil { - fmt.Println("[Orchid] Failed to create config file: ", err) - return subcommands.ExitFailure + return fmt.Errorf("failed to create config file: %w", err) } + defer createConf.Close() - confEncode := yaml.NewEncoder(createConf) - confEncode.SetIndent(2) - err = confEncode.Encode(startUpConfig{ + // this is the whole config structure + config := startUpConfig{ Listen: answers.ApiListen, Acme: acmeConfig{ PresentUrl: answers.AcmePresentUrl, @@ -155,18 +129,20 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa Certificate: "default", }, Domains: strings.Split(answers.ApiDomains, ","), - }) + } + + confEncode := yaml.NewEncoder(createConf) + confEncode.SetIndent(2) + err = confEncode.Encode(config) if err != nil { - fmt.Println("[Orchid] Failed to write config file: ", err) - return subcommands.ExitFailure + return fmt.Errorf("failed to write config file: %w", err) } // write token file - tokenFile := filepath.Join(wdAbs, "tokens.yml") + tokenFile := filepath.Join(wd, "tokens.yml") createTokens, err := os.Create(tokenFile) if err != nil { - fmt.Println("[Orchid] Failed to create tokens file: ", err) - return subcommands.ExitFailure + return fmt.Errorf("failed to create tokens file: %w", err) } confEncode = yaml.NewEncoder(createTokens) @@ -176,14 +152,13 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa Refresh: answers.AcmeRefresh, }) if err != nil { - fmt.Println("[Orchid] Failed to write tokens file: ", err) - return subcommands.ExitFailure + return fmt.Errorf("failed to write tokens file: %w", err) } - fmt.Println("[Orchid] Setup complete") - fmt.Printf("[Orchid] Run the renewal service with `orchid serve -conf %s`\n", confFile) + logger.Logger.Info("Setup complete") + logger.Logger.Infof("Run the renewal service with `orchid-daemon -conf %s`", confFile) - return subcommands.ExitSuccess + return nil } func listenAddressValidator(ans interface{}) error { diff --git a/go.mod b/go.mod index aafe696..b12e0f9 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/go-acme/lego/v4 v4.21.0 github.com/golang-jwt/jwt/v4 v4.5.1 github.com/golang-migrate/migrate/v4 v4.18.2 - github.com/google/subcommands v1.2.0 github.com/google/uuid v1.6.0 github.com/julienschmidt/httprouter v1.3.0 github.com/mattn/go-sqlite3 v1.14.24 diff --git a/go.sum b/go.sum index 93de238..82b683c 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,6 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=