diff options
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 3 | ||||
-rw-r--r-- | gomod2nix.toml | 3 | ||||
-rw-r--r-- | import/main.go | 116 | ||||
-rw-r--r-- | internal/config/config.go | 157 | ||||
-rw-r--r-- | internal/importer/main.go | 90 | ||||
-rw-r--r-- | internal/server/headers.go | 4 | ||||
-rw-r--r-- | internal/server/mux.go | 65 | ||||
-rw-r--r-- | internal/server/server.go | 7 | ||||
-rw-r--r-- | nix/modules/default.nix | 135 | ||||
-rw-r--r-- | serve/main.go | 24 |
11 files changed, 306 insertions, 299 deletions
diff --git a/go.mod b/go.mod index 1a81e20..17d0ed4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module searchix go 1.22.2 require ( - github.com/ardanlabs/conf/v3 v3.1.7 github.com/bcicen/jstream v1.0.1 github.com/blevesearch/bleve/v2 v2.4.0 github.com/blevesearch/bleve_index_api v1.1.6 diff --git a/go.sum b/go.sum index a61a78e..519ca16 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= -github.com/ardanlabs/conf/v3 v3.1.7 h1:p232cF68TafoA5U9ZlbxUIhGJtGNdKHBXF80Fdqb5t0= -github.com/ardanlabs/conf/v3 v3.1.7/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU= github.com/bcicen/jstream v1.0.1 h1:BXY7Cu4rdmc0rhyTVyT3UkxAiX3bnLpKLas9btbH5ck= github.com/bcicen/jstream v1.0.1/go.mod h1:9ielPxqFry7Y4Tg3j4BfjPocfJ3TbsRtXOAYXYmRuAQ= github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= @@ -61,7 +59,6 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/gomod2nix.toml b/gomod2nix.toml index 047d995..12e719c 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -4,9 +4,6 @@ schema = 3 [mod."github.com/RoaringBitmap/roaring"] version = "v1.9.3" hash = "sha256-LZfRufkU4UhuEcgxuCPd6divX2KIdcHp1FOt79mQV7Q=" - [mod."github.com/ardanlabs/conf/v3"] - version = "v3.1.7" - hash = "sha256-7H53l0JN5Q6hkAgBivVQ8lFd03oNmP1IG8ihzLKm2CQ=" [mod."github.com/bcicen/jstream"] version = "v1.0.1" hash = "sha256-mm+/BuIEYYj6XOHCCJLxVMKd1XcBXCiRCWA+aTvr1sE=" diff --git a/import/main.go b/import/main.go index d9c3bf5..76ebdcf 100644 --- a/import/main.go +++ b/import/main.go @@ -1,125 +1,29 @@ package main import ( - "context" - "errors" + "flag" "log" "log/slog" - "os" - "os/exec" - "path" "searchix/internal/config" "searchix/internal/importer" - "searchix/internal/search" - "strings" - "time" - - "github.com/ardanlabs/conf/v3" ) -type Config struct { - ConfigFile string `conf:"short:c"` - LogLevel slog.Level `conf:"default:INFO"` - Timeout time.Duration `conf:"default:30m,help:maximum time to wait for all fetchers and importers combined"` - Replace bool `conf:"default:false,help:whether to remove existing database, if exists"` -} +var ( + replace = flag.Bool("replace", false, "whether to replace existing database, if it exists") + configFile = flag.String("config", "config.toml", "config file to use") +) func main() { - if _, found := os.LookupEnv("DEBUG"); found { - slog.SetLogLoggerLevel(slog.LevelDebug) - } - var runtimeConfig Config - help, err := conf.Parse("", &runtimeConfig) - if err != nil { - if errors.Is(err, conf.ErrHelpWanted) { - log.Println(help) - os.Exit(1) - } - log.Panicf("parsing runtime configuration: %v", err) - } - slog.SetLogLoggerLevel(runtimeConfig.LogLevel) + flag.Parse() - cfg, err := config.GetConfig(runtimeConfig.ConfigFile) + cfg, err := config.GetConfig(*configFile) if err != nil { log.Fatal(err) } + slog.SetLogLoggerLevel(cfg.LogLevel) - if len(cfg.Sources) == 0 { - slog.Info("No sources enabled") - - return - } - - indexer, err := search.NewIndexer(cfg.DataPath, runtimeConfig.Replace) + err = importer.Start(cfg, *replace) if err != nil { - log.Fatalf("Failed to create indexer: %v", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), runtimeConfig.Timeout) - defer cancel() - - var imp importer.Importer - var hadErrors bool - for name, source := range cfg.Sources { - logger := slog.With("name", name, "importer", source.Type.String()) - logger.Debug("starting importer") - - importerDataPath := path.Join(cfg.DataPath, "sources", source.Channel) - - switch source.Type { - case importer.ChannelNixpkgs: - imp = importer.NewNixpkgsChannelImporter(source, importerDataPath, logger) - case importer.Channel: - imp = importer.NewChannelImporter(source, importerDataPath, logger) - default: - log.Printf("unsupported importer type %s", source.Type.String()) - - continue - } - - updated, err := imp.FetchIfNeeded(ctx) - - if err != nil { - var exerr *exec.ExitError - if errors.As(err, &exerr) { - lines := strings.Split(strings.TrimSpace(string(exerr.Stderr)), "\n") - for _, line := range lines { - logger.Warn("importer fetch failed", "stderr", line, "status", exerr.ExitCode()) - } - } else { - logger.Warn("importer fetch failed", "error", err) - } - hadErrors = true - - continue - } - logger.Info("importer fetch succeeded", "updated", updated) - - if updated || runtimeConfig.Replace { - hadWarnings, err := imp.Import(ctx, indexer) - - if err != nil { - msg := err.Error() - for _, line := range strings.Split(strings.TrimSpace(msg), "\n") { - logger.Error("importer init failed", "error", line) - } - - continue - } - if hadWarnings { - logger.Warn("importer succeeded, but with warnings/errors") - } else { - logger.Info("importer succeeded") - } - } - } - - err = indexer.Close() - if err != nil { - slog.Error("error closing indexer", "error", err) - } - - if hadErrors { - defer os.Exit(1) + log.Fatal(err) } } diff --git a/internal/config/config.go b/internal/config/config.go index 340b027..c3a5a90 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,75 +26,108 @@ func (u *URL) UnmarshalText(text []byte) (err error) { return nil } +func mustURL(in string) (u URL) { + var err error + u.URL, err = url.Parse(in) + if err != nil { + panic(errors.Errorf("URL cannot be parsed: %s", in)) + } + + return u +} + +type Web struct { + ContentSecurityPolicy CSP + ListenAddress string + Port string + BaseURL URL + SentryDSN string + Environment string + ExtraBodyHTML template.HTML + Headers map[string]string +} + +type Importer struct { + Sources map[string]*Source + Timeout time.Duration +} + type Config struct { - DataPath string `toml:"data-path"` - CSP CSP `toml:"content-security-policy"` - ExtraBodyHTML template.HTML `toml:"extra-body-html"` - Headers map[string]string - Sources map[string]*Source + DataPath string + LogLevel slog.Level + Web *Web + Importer *Importer } var defaultConfig = Config{ DataPath: "./data", - CSP: CSP{ - DefaultSrc: []string{"'self'"}, - }, - Headers: map[string]string{ - "x-content-type-options": "nosniff", + Web: &Web{ + ListenAddress: "localhost", + Port: "3000", + BaseURL: mustURL("http://localhost:3000"), + ContentSecurityPolicy: CSP{ + DefaultSrc: []string{"'self'"}, + }, + Headers: map[string]string{ + "x-content-type-options": "nosniff", + }, }, - Sources: map[string]*Source{ - "nixos": { - Name: "NixOS", - Key: "nixos", - Enable: true, - Type: Channel, - Channel: "nixpkgs", - URL: "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz", - ImportPath: "nixos/release.nix", - Attribute: "options", - OutputPath: "share/doc/nixos/options.json", - FetchTimeout: 5 * time.Minute, - ImportTimeout: 15 * time.Minute, - Repo: Repository{ - Type: "github", - Owner: "NixOS", - Repo: "nixpkgs", + Importer: &Importer{ + Timeout: 30 * time.Minute, + Sources: map[string]*Source{ + "nixos": { + Name: "NixOS", + Key: "nixos", + Enable: true, + Type: Channel, + Channel: "nixpkgs", + URL: "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz", + ImportPath: "nixos/release.nix", + Attribute: "options", + OutputPath: "share/doc/nixos/options.json", + FetchTimeout: 5 * time.Minute, + ImportTimeout: 15 * time.Minute, + Repo: Repository{ + Type: "github", + Owner: "NixOS", + Repo: "nixpkgs", + }, }, - }, - "darwin": { - Name: "Darwin", - Key: "darwin", - Enable: false, - Type: Channel, - Channel: "darwin", - URL: "https://github.com/LnL7/nix-darwin/archive/master.tar.gz", - ImportPath: "release.nix", - Attribute: "options", - OutputPath: "share/doc/darwin/options.json", - FetchTimeout: 5 * time.Minute, - ImportTimeout: 15 * time.Minute, - Repo: Repository{ - Type: "github", - Owner: "LnL7", - Repo: "nix-darwin", + "darwin": { + Name: "Darwin", + Key: "darwin", + Enable: false, + Type: Channel, + Channel: "darwin", + URL: "https://github.com/LnL7/nix-darwin/archive/master.tar.gz", + ImportPath: "release.nix", + Attribute: "options", + OutputPath: "share/doc/darwin/options.json", + FetchTimeout: 5 * time.Minute, + ImportTimeout: 15 * time.Minute, + Repo: Repository{ + Type: "github", + Owner: "LnL7", + Repo: "nix-darwin", + }, }, - }, - "home-manager": { - Name: "Home Manager", - Key: "home-manager", - Enable: false, - Channel: "home-manager", - URL: "https://github.com/nix-community/home-manager/archive/master.tar.gz", - Type: Channel, - ImportPath: "default.nix", - Attribute: "docs.json", - OutputPath: "share/doc/home-manager/options.json", - FetchTimeout: 5 * time.Minute, - ImportTimeout: 15 * time.Minute, - Repo: Repository{ - Type: "github", - Owner: "nix-community", - Repo: "home-manager", + "home-manager": { + Name: "Home Manager", + Key: "home-manager", + Enable: false, + Channel: "home-manager", + URL: "https://github.com/nix-community/home-manager/archive/master.tar.gz", + Type: Channel, + ImportPath: "default.nix", + Attribute: "docs.json", + OutputPath: "share/doc/home-manager/options.json", + FetchTimeout: 5 * time.Minute, + ImportTimeout: 15 * time.Minute, + Repo: Repository{ + Type: "github", + Owner: "nix-community", + Repo: "home-manager", + }, }, }, }, @@ -122,7 +155,7 @@ func GetConfig(filename string) (*Config, error) { } } - maps.DeleteFunc(config.Sources, func(_ string, v *Source) bool { + maps.DeleteFunc(config.Importer.Sources, func(_ string, v *Source) bool { return !v.Enable }) diff --git a/internal/importer/main.go b/internal/importer/main.go new file mode 100644 index 0000000..a6f15e9 --- /dev/null +++ b/internal/importer/main.go @@ -0,0 +1,90 @@ +package importer + +import ( + "context" + "errors" + "log" + "log/slog" + "os/exec" + "path" + "searchix/internal/config" + "searchix/internal/search" + "strings" +) + +func Start(cfg *config.Config, replace bool) error { + if len(cfg.Importer.Sources) == 0 { + slog.Info("No sources enabled") + + return nil + } + + indexer, err := search.NewIndexer(cfg.DataPath, replace) + if err != nil { + log.Fatalf("Failed to create indexer: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), cfg.Importer.Timeout) + defer cancel() + + var imp Importer + for name, source := range cfg.Importer.Sources { + logger := slog.With("name", name, "importer", source.Type.String()) + logger.Debug("starting importer") + + importerDataPath := path.Join(cfg.DataPath, "sources", source.Channel) + + switch source.Type { + case config.ChannelNixpkgs: + imp = NewNixpkgsChannelImporter(source, importerDataPath, logger) + case config.Channel: + imp = NewChannelImporter(source, importerDataPath, logger) + default: + log.Printf("unsupported importer type %s", source.Type.String()) + + continue + } + + updated, err := imp.FetchIfNeeded(ctx) + + if err != nil { + var exerr *exec.ExitError + if errors.As(err, &exerr) { + lines := strings.Split(strings.TrimSpace(string(exerr.Stderr)), "\n") + for _, line := range lines { + logger.Warn("importer fetch failed", "stderr", line, "status", exerr.ExitCode()) + } + } else { + logger.Warn("importer fetch failed", "error", err) + } + + continue + } + logger.Info("importer fetch succeeded", "updated", updated) + + if updated || replace { + hadWarnings, err := imp.Import(ctx, indexer) + + if err != nil { + msg := err.Error() + for _, line := range strings.Split(strings.TrimSpace(msg), "\n") { + logger.Error("importer init failed", "error", line) + } + + continue + } + if hadWarnings { + logger.Warn("importer succeeded, but with warnings/errors") + } else { + logger.Info("importer succeeded") + } + } + } + + err = indexer.Close() + if err != nil { + slog.Error("error closing indexer", "error", err) + } + + return nil +} diff --git a/internal/server/headers.go b/internal/server/headers.go index 0efc384..4fb9efd 100644 --- a/internal/server/headers.go +++ b/internal/server/headers.go @@ -7,10 +7,10 @@ import ( func AddHeadersMiddleware(next http.Handler, config *cfg.Config) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - for h, v := range config.Headers { + for h, v := range config.Web.Headers { w.Header().Add(h, v) } - w.Header().Add("Content-Security-Policy", config.CSP.String()) + w.Header().Add("Content-Security-Policy", config.Web.ContentSecurityPolicy.String()) next.ServeHTTP(w, r) }) diff --git a/internal/server/mux.go b/internal/server/mux.go index ac128ce..9d3b29a 100644 --- a/internal/server/mux.go +++ b/internal/server/mux.go @@ -15,8 +15,7 @@ import ( "time" "searchix/frontend" - cfg "searchix/internal/config" - "searchix/internal/importer" + "searchix/internal/config" "searchix/internal/options" "searchix/internal/search" @@ -28,19 +27,6 @@ import ( "github.com/shengyanli1982/law" ) -var config *cfg.Config - -type Config struct { - Environment string `conf:"default:development"` - LiveReload bool `conf:"default:false,flag:live"` - ListenAddress string `conf:"default:localhost"` - Port string `conf:"default:3000,short:p"` - BaseURL cfg.URL `conf:"default:http://localhost:3000,short:b"` - ConfigFile string `conf:"short:c"` - LogLevel slog.Level `conf:"default:INFO"` - SentryDSN string -} - type HTTPError struct { Error error Message string @@ -50,8 +36,8 @@ type HTTPError struct { const jsSnippet = template.HTML(livereload.JsSnippet) // #nosec G203 type TemplateData struct { - Sources map[string]*importer.Source - Source importer.Source + Sources map[string]*config.Source + Source config.Source Query string Results bool SourceResult *bleve.SearchResult @@ -67,20 +53,17 @@ type ResultData[T options.NixOption] struct { Next string } -func applyDevModeOverrides(config *cfg.Config) { - if len(config.CSP.ScriptSrc) == 0 { - config.CSP.ScriptSrc = config.CSP.DefaultSrc +func applyDevModeOverrides(config *config.Config) { + if len(config.Web.ContentSecurityPolicy.ScriptSrc) == 0 { + config.Web.ContentSecurityPolicy.ScriptSrc = config.Web.ContentSecurityPolicy.DefaultSrc } - config.CSP.ScriptSrc = append(config.CSP.ScriptSrc, "'unsafe-inline'") + config.Web.ContentSecurityPolicy.ScriptSrc = append( + config.Web.ContentSecurityPolicy.ScriptSrc, + "'unsafe-inline'", + ) } -func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { - var err error - config, err = cfg.GetConfig(runtimeConfig.ConfigFile) - if err != nil { - return nil, errors.WithMessage(err, "error parsing configuration file") - } - +func NewMux(config *config.Config, liveReload bool) (*http.ServeMux, error) { slog.Debug("loading index") index, err := search.Open(config.DataPath) slog.Debug("loaded index") @@ -91,8 +74,8 @@ func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { err = sentry.Init(sentry.ClientOptions{ EnableTracing: true, TracesSampleRate: 1.0, - Dsn: runtimeConfig.SentryDSN, - Environment: runtimeConfig.Environment, + Dsn: config.Web.SentryDSN, + Environment: config.Web.Environment, }) if err != nil { return nil, errors.WithMessage(err, "could not set up sentry") @@ -111,8 +94,8 @@ func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { mux := http.NewServeMux() mux.HandleFunc("/{$}", func(w http.ResponseWriter, _ *http.Request) { indexData := TemplateData{ - ExtraBodyHTML: config.ExtraBodyHTML, - Sources: config.Sources, + ExtraBodyHTML: config.Web.ExtraBodyHTML, + Sources: config.Importer.Sources, } err := templates["index"].ExecuteTemplate(w, "index.gotmpl", indexData) if err != nil { @@ -124,7 +107,7 @@ func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { mux.HandleFunc("/options/{source}/search", func(w http.ResponseWriter, r *http.Request) { sourceKey := r.PathValue("source") - source := config.Sources[sourceKey] + source := config.Importer.Sources[sourceKey] if source == nil { http.Error(w, "Source not found", http.StatusNotFound) @@ -157,9 +140,9 @@ func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { tdata := ResultData[options.NixOption]{ TemplateData: TemplateData{ - ExtraBodyHTML: config.ExtraBodyHTML, + ExtraBodyHTML: config.Web.ExtraBodyHTML, Source: *source, - Sources: config.Sources, + Sources: config.Importer.Sources, }, ResultsPerPage: search.ResultsPerPage, Query: qs, @@ -216,8 +199,8 @@ func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { } err = templates["search"].Execute(w, TemplateData{ - ExtraBodyHTML: config.ExtraBodyHTML, - Sources: config.Sources, + ExtraBodyHTML: config.Web.ExtraBodyHTML, + Sources: config.Importer.Sources, Source: *source, SourceResult: sourceResult, }) @@ -231,9 +214,9 @@ func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { mux.Handle("/static/", http.FileServer(http.FS(frontend.Files))) - if runtimeConfig.LiveReload { + if liveReload { applyDevModeOverrides(config) - config.ExtraBodyHTML = jsSnippet + config.Web.ExtraBodyHTML = jsSnippet liveReload := livereload.New() liveReload.Start() top.Handle("/livereload", liveReload) @@ -258,7 +241,7 @@ func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { } var logWriter io.Writer - if runtimeConfig.Environment == "production" { + if config.Web.Environment == "production" { logWriter = law.NewWriteAsyncer(os.Stdout, nil) } else { logWriter = os.Stdout @@ -267,7 +250,7 @@ func NewMux(runtimeConfig *Config) (*http.ServeMux, error) { AddHeadersMiddleware( sentryHandler.Handle( wrapHandlerWithLogging(mux, wrappedHandlerOptions{ - defaultHostname: runtimeConfig.BaseURL.Hostname(), + defaultHostname: config.Web.BaseURL.Hostname(), logger: logWriter, }), ), diff --git a/internal/server/server.go b/internal/server/server.go index d13d031..77163d3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -6,6 +6,7 @@ import ( "log/slog" "net" "net/http" + "searchix/internal/config" "time" "github.com/pkg/errors" @@ -15,12 +16,12 @@ type Server struct { *http.Server } -func New(runtimeConfig *Config) (*Server, error) { - mux, err := NewMux(runtimeConfig) +func New(conf *config.Config, liveReload bool) (*Server, error) { + mux, err := NewMux(conf, liveReload) if err != nil { return nil, err } - listenAddress := net.JoinHostPort(runtimeConfig.ListenAddress, runtimeConfig.Port) + listenAddress := net.JoinHostPort(conf.Web.ListenAddress, conf.Web.Port) return &Server{ &http.Server{ diff --git a/nix/modules/default.nix b/nix/modules/default.nix index 52a8c93..62cc5c0 100644 --- a/nix/modules/default.nix +++ b/nix/modules/default.nix @@ -12,16 +12,6 @@ let settingsFormat = pkgs.formats.toml { }; - env = { - ENVIRONMENT = "production"; - LISTEN_ADDRESS = cfg.listenAddress; - PORT = (toString cfg.port); - BASE_URL = cfg.baseUrl; - CONFIG_FILE = settingsFormat.generate "searchix-config.toml" cfg.settings; - LOG_LEVEL = cfg.logLevel; - SENTRY_DSN = cfg.sentryDsn; - }; - defaultServiceConfig = { User = cfg.user; Group = cfg.group; @@ -86,69 +76,84 @@ in example = "weekly"; }; - port = mkOption { - type = types.port; - description = "Port for searchix to listen on"; - default = 51313; - }; - - listenAddress = mkOption { - type = types.str; - description = "Listen on a specific IP address."; - default = "localhost"; - }; - - baseUrl = mkOption { - type = types.str; - description = "The base URL that searchix will be served on."; - default = "http://localhost:3000"; - }; - - sentryDsn = mkOption { - type = with types; nullOr str; - description = "Optionally enable sentry to track errors."; - default = null; - }; - - logLevel = mkOption { - type = with types; enum [ "error" "warn" "info" "debug" ]; - description = "Only log messages with the given severity or above."; - default = "info"; - }; - - importTimeout = mkOption { - type = types.str; - default = "30m"; - description = '' - Maximum time to wait for all import jobs. - May need to be increased based on the number of sources. - ''; - }; - settings = mkOption { type = types.submodule { freeformType = settingsFormat.type; options = { - data-path = mkOption { + dataPath = mkOption { type = types.str; - description = "Where to store search index and other data, can be relative to homeDir."; + description = "Where to store search index and other data."; default = "${cfg.homeDir}/data"; }; - sources = mkOption { - type = with types; attrsOf (submodule (import ./source-options.nix { - inherit cfg settingsFormat; - })); - default = { - nixos.enable = true; - darwin.enable = false; - home-manager.enable = false; + + logLevel = mkOption { + type = with types; enum [ "error" "warn" "info" "debug" ]; + description = "Only log messages with the given severity or above."; + default = "info"; + }; + + web = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + port = mkOption { + type = types.port; + description = "Port for searchix to listen on"; + default = 51313; + }; + + listenAddress = mkOption { + type = types.str; + description = "Listen on a specific IP address."; + default = "localhost"; + }; + + baseURL = mkOption { + type = types.str; + description = "The base URL that searchix will be served on."; + default = "http://localhost:3000"; + }; + + sentryDSN = mkOption { + type = with types; nullOr str; + description = "Optionally enable sentry to track errors."; + default = null; + }; + }; + }; + }; + + importer = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + + importTimeout = mkOption { + type = types.str; + default = "30m"; + description = '' + Maximum time to wait for all import jobs. + May need to be increased based on the number of sources. + ''; + }; + + sources = mkOption { + type = with types; + attrsOf (submodule (import ./source-options.nix { + inherit cfg settingsFormat; + })); + default = { + nixos.enable = true; + darwin.enable = false; + home-manager.enable = false; + }; + description = "Declarative specification of options sources for searchix."; + }; }; - description = "Declarative specification of options sources for searchix."; }; }; }; default = { }; - description = "Configuration for searchix (TODO: publish description)."; + description = "Configuration for searchix (a web search interface for options in the nix ecosystem)."; }; }; @@ -156,17 +161,16 @@ in systemd.services.searchix-importer = { description = "Searchix option importer"; conflicts = [ "searchix-web.service" ]; - before = [ "searchix-web.service" ]; path = with pkgs; [ nix ]; serviceConfig = defaultServiceConfig // { - ExecStart = "${package}/bin/import"; + ExecStart = "${package}/bin/import --config ${(settingsFormat.generate "searchix-config.toml" cfg.settings)}"; Type = "oneshot"; RestartSec = 10; RestartSteps = 5; RestartMaxDelaySec = "5 min"; }; - environment = env; + startAt = cfg.dates; }; @@ -182,9 +186,8 @@ in after = [ "searchix-importer.service" ]; wants = [ "searchix-importer.service" ]; wantedBy = [ "multi-user.target" ]; - environment = env; serviceConfig = defaultServiceConfig // { - ExecStart = "${package}/bin/serve"; + ExecStart = "${package}/bin/serve --config ${(settingsFormat.generate "searchix-config.toml" cfg.settings)}"; } // lib.optionalAttrs (cfg.port < 1024) { AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; diff --git a/serve/main.go b/serve/main.go index 9ba2113..709d340 100644 --- a/serve/main.go +++ b/serve/main.go @@ -1,36 +1,36 @@ package main import ( - "fmt" + "flag" "log" "log/slog" "os" "os/signal" "sync" + "searchix/internal/config" "searchix/internal/server" +) - "github.com/ardanlabs/conf/v3" - "github.com/pkg/errors" +var ( + liveReload = flag.Bool("live", false, "whether to enable live reloading (development)") + configFile = flag.String("config", "config.toml", "config file to use") ) func main() { - runtimeConfig := server.Config{} - help, err := conf.Parse("", &runtimeConfig) + flag.Parse() + + conf, err := config.GetConfig(*configFile) if err != nil { - if errors.Is(err, conf.ErrHelpWanted) { - fmt.Println(help) - os.Exit(1) - } - log.Panicf("parsing runtime configuration: %v", err) + log.Panicf("error parsing configuration file: %v", err) } - slog.SetLogLoggerLevel(runtimeConfig.LogLevel) + slog.SetLogLoggerLevel(conf.LogLevel) log.SetFlags(log.LstdFlags | log.Lmsgprefix) log.SetPrefix("searchix: ") c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt) - sv, err := server.New(&runtimeConfig) + sv, err := server.New(conf, *liveReload) if err != nil { log.Fatalf("error setting up server: %v", err) } |