diff --git a/cmd/ugitd/args.gen.go b/cmd/ugitd/args.gen.go deleted file mode 100644 index e8b94bb2d8d2a7a80def9d1da2c4b4ae06bd0c28..0000000000000000000000000000000000000000 --- a/cmd/ugitd/args.gen.go +++ /dev/null @@ -1,184 +0,0 @@ -// Code generated by gen from schema.cue; DO NOT EDIT. - -package main - -import ( - "errors" - "flag" - "fmt" - "log/slog" - "regexp" -) - -var ( - reURL = regexp.MustCompile("^[a-z][a-z0-9+.-]*://") -) - -type cliArgs struct { - // Path to directory containing repositories - RepoDir string - SSH sshArgs - HTTP httpArgs - Meta metaArgs - Profile profileArgs - Log logArgs - // Show private repos in web interface - ShowPrivate bool -} - -type sshArgs struct { - // Enable SSH server - Enable bool - // Path to authorized_keys - AuthorizedKeys string - // SSH clone URL base - CloneURL string - // SSH port - Port int - // SSH host key (created if it doesn't exist) - HostKey string -} - -type httpArgs struct { - // Enable HTTP server - Enable bool - // HTTP clone URL base - CloneURL string - // HTTP port - Port int -} - -type metaArgs struct { - // App title - Title string - // App description - Description string -} - -type profileArgs struct { - // Username for index page - Username string - // Email for index page - Email string - // Link(s) for index page - Links []profileLink -} - -type logArgs struct { - // Logging level - Level slog.Level - // Print logs in JSON(L) format - JSON bool -} - -// defaultArgs returns the defaults from schema.cue. -func defaultArgs() cliArgs { - return cliArgs{ - RepoDir: ".ugit", - SSH: sshArgs{ - Enable: true, - AuthorizedKeys: ".ssh/authorized_keys", - CloneURL: "ssh://localhost:8448", - Port: 8448, - HostKey: ".ssh/ugit_ed25519", - }, - HTTP: httpArgs{ - Enable: true, - CloneURL: "http://localhost:8449", - Port: 8449, - }, - Meta: metaArgs{ - Title: "ugit", - Description: "Minimal git server", - }, - Log: logArgs{ - Level: mustParse(parseLogLevel, "error"), - }, - } -} - -// registerFlags defines a flag for every field of c, using c's current -// values as flag defaults. -func registerFlags(fs *flag.FlagSet, c *cliArgs) { - fs.StringVar(&c.RepoDir, "repo-dir", c.RepoDir, "Path to directory containing repositories") - fs.BoolVar(&c.SSH.Enable, "ssh.enable", c.SSH.Enable, "Enable SSH server") - fs.StringVar(&c.SSH.AuthorizedKeys, "ssh.authorized-keys", c.SSH.AuthorizedKeys, "Path to authorized_keys") - fs.StringVar(&c.SSH.CloneURL, "ssh.clone-url", c.SSH.CloneURL, "SSH clone URL base") - fs.IntVar(&c.SSH.Port, "ssh.port", c.SSH.Port, "SSH port") - fs.StringVar(&c.SSH.HostKey, "ssh.host-key", c.SSH.HostKey, "SSH host key (created if it doesn't exist)") - fs.BoolVar(&c.HTTP.Enable, "http.enable", c.HTTP.Enable, "Enable HTTP server") - fs.StringVar(&c.HTTP.CloneURL, "http.clone-url", c.HTTP.CloneURL, "HTTP clone URL base") - fs.IntVar(&c.HTTP.Port, "http.port", c.HTTP.Port, "HTTP port") - fs.StringVar(&c.Meta.Title, "meta.title", c.Meta.Title, "App title") - fs.StringVar(&c.Meta.Description, "meta.description", c.Meta.Description, "App description") - fs.StringVar(&c.Profile.Username, "profile.username", c.Profile.Username, "Username for index page") - fs.StringVar(&c.Profile.Email, "profile.email", c.Profile.Email, "Email for index page") - { - set := false - fs.Func("profile.links", "Link(s) for index page (repeatable)", func(s string) error { - v, err := parseProfileLink(s) - if err != nil { - return err - } - if !set { - c.Profile.Links, set = nil, true - } - c.Profile.Links = append(c.Profile.Links, v) - return nil - }) - } - fs.Func("log.level", "Logging level (one of: debug, info, warn, warning, error; default: error)", func(s string) error { - v, err := parseLogLevel(s) - if err != nil { - return err - } - c.Log.Level = v - return nil - }) - fs.BoolVar(&c.Log.JSON, "log.json", c.Log.JSON, "Print logs in JSON(L) format") - fs.BoolVar(&c.ShowPrivate, "show-private", c.ShowPrivate, "Show private repos in web interface") -} - -// validate checks c against the constraints in schema.cue. -func (c *cliArgs) validate() error { - var errs []error - if c.RepoDir == "" { - errs = append(errs, errors.New("repo-dir: must not be empty")) - } - if c.SSH.AuthorizedKeys == "" { - errs = append(errs, errors.New("ssh.authorized-keys: must not be empty")) - } - if !reURL.MatchString(c.SSH.CloneURL) { - errs = append(errs, fmt.Errorf("ssh.clone-url: %q does not match %s", c.SSH.CloneURL, reURL)) - } - if c.SSH.Port < 1 { - errs = append(errs, fmt.Errorf("ssh.port: must be >= 1, got %v", c.SSH.Port)) - } - if c.SSH.Port > 65535 { - errs = append(errs, fmt.Errorf("ssh.port: must be <= 65535, got %v", c.SSH.Port)) - } - if c.SSH.HostKey == "" { - errs = append(errs, errors.New("ssh.host-key: must not be empty")) - } - if !reURL.MatchString(c.HTTP.CloneURL) { - errs = append(errs, fmt.Errorf("http.clone-url: %q does not match %s", c.HTTP.CloneURL, reURL)) - } - if c.HTTP.Port < 1 { - errs = append(errs, fmt.Errorf("http.port: must be >= 1, got %v", c.HTTP.Port)) - } - if c.HTTP.Port > 65535 { - errs = append(errs, fmt.Errorf("http.port: must be <= 65535, got %v", c.HTTP.Port)) - } - return errors.Join(errs...) -} - -// mustParse applies a hand-written parse hook to a schema default. gen has -// already checked the default against the schema, so a failure here means -// the hook and the schema disagree. -func mustParse[T any](parse func(string) (T, error), s string) T { - v, err := parse(s) - if err != nil { - panic(fmt.Sprintf("schema default %q rejected by parse hook: %v", s, err)) - } - return v -} diff --git a/cmd/ugitd/args.go b/cmd/ugitd/args.go index 3ac12922bc22496c5c381aeebb29a19284bf0908..e7b42a6037ee8c1e80de0459bac6f126e76814d7 100644 --- a/cmd/ugitd/args.go +++ b/cmd/ugitd/args.go @@ -1,7 +1,5 @@ package main -//go:generate go tool gen -schema schema.cue -go args.gen.go -nix ../../nix/options.gen.nix - import ( _ "embed" "flag" @@ -16,42 +14,39 @@ //go:embed schema.cue var schema string -func parseArgs(args []string) (cliArgs, error) { - fs := flag.NewFlagSet("ugitd", flag.ContinueOnError) - fs.String("config", "ugit.jsonnet", "Path to config file") +type cliArgs struct { + RepoDir string + SSH sshArgs + HTTP httpArgs + Meta metaArgs + Profile profileArgs + Log logArgs + ShowPrivate bool +} - c := defaultArgs() - registerFlags(fs, &c) +type sshArgs struct { + Enable bool + AuthorizedKeys string + CloneURL string + Port int + HostKey string +} - parser := ffjsonnet.ParseConfig{Schema: schema} - err := ff.Parse(fs, args, - ff.WithEnvVarPrefix("UGIT"), - ff.WithConfigFileFlag("config"), - ff.WithAllowMissingConfigFile(true), - ff.WithConfigFileParser(parser.Parse), - ) - if err != nil { - return c, err - } +type httpArgs struct { + Enable bool + CloneURL string + Port int +} - return c, c.validate() +type metaArgs struct { + Title string + Description string } -func parseLogLevel(s string) (slog.Level, error) { - var lvl slog.Level - switch strings.ToLower(s) { - case "debug": - lvl = slog.LevelDebug - case "info": - lvl = slog.LevelInfo - case "warn", "warning": - lvl = slog.LevelWarn - case "error": - lvl = slog.LevelError - default: - return -1, fmt.Errorf("unknown log level %q: options are [debug, info, warn, error]", s) - } - return lvl, nil +type profileArgs struct { + Username string + Email string + Links []profileLink } type profileLink struct { @@ -59,13 +54,89 @@ Name string URL string } -func parseProfileLink(s string) (profileLink, error) { - parts := strings.SplitN(s, ",", 2) - if len(parts) != 2 { - return profileLink{}, fmt.Errorf("invalid profile link %q", s) +type logArgs struct { + Level slog.Level + JSON bool +} + +func parseArgs(args []string) (c cliArgs, e error) { + fs := flag.NewFlagSet("ugitd", flag.ContinueOnError) + fs.String("config", "ugit.jsonnet", "Path to config file") + + c = cliArgs{ + RepoDir: ".ugit", + SSH: sshArgs{ + Enable: true, + AuthorizedKeys: ".ssh/authorized_keys", + CloneURL: "ssh://localhost:8448", + Port: 8448, + HostKey: ".ssh/ugit_ed25519", + }, + HTTP: httpArgs{ + Enable: true, + CloneURL: "http://localhost:8449", + Port: 8449, + }, + Meta: metaArgs{ + Title: "ugit", + Description: "Minimal git server", + }, + Log: logArgs{ + Level: slog.LevelError, + }, } - return profileLink{ - Name: parts[0], - URL: parts[1], - }, nil + + fs.Func("log.level", "Logging level", func(s string) error { + var lvl slog.Level + switch strings.ToLower(s) { + case "debug": + lvl = slog.LevelDebug + case "info": + lvl = slog.LevelInfo + case "warn", "warning": + lvl = slog.LevelWarn + case "error": + lvl = slog.LevelError + default: + return fmt.Errorf("unknown log level %q: options are [debug, info, warn, error]", s) + } + c.Log.Level = lvl + return nil + }) + fs.BoolVar(&c.Log.JSON, "log.json", c.Log.JSON, "Print logs in JSON(L) format") + fs.StringVar(&c.RepoDir, "repo-dir", c.RepoDir, "Path to directory containing repositories") + fs.BoolVar(&c.ShowPrivate, "show-private", c.ShowPrivate, "Show private repos in web interface") + fs.BoolVar(&c.SSH.Enable, "ssh.enable", c.SSH.Enable, "Enable SSH server") + fs.StringVar(&c.SSH.AuthorizedKeys, "ssh.authorized-keys", c.SSH.AuthorizedKeys, "Path to authorized_keys") + fs.StringVar(&c.SSH.CloneURL, "ssh.clone-url", c.SSH.CloneURL, "SSH clone URL base") + fs.IntVar(&c.SSH.Port, "ssh.port", c.SSH.Port, "SSH port") + fs.StringVar(&c.SSH.HostKey, "ssh.host-key", c.SSH.HostKey, "SSH host key (created if it doesn't exist)") + fs.BoolVar(&c.HTTP.Enable, "http.enable", c.HTTP.Enable, "Enable HTTP server") + fs.StringVar(&c.HTTP.CloneURL, "http.clone-url", c.HTTP.CloneURL, "HTTP clone URL base") + fs.IntVar(&c.HTTP.Port, "http.port", c.HTTP.Port, "HTTP port") + fs.StringVar(&c.Meta.Title, "meta.title", c.Meta.Title, "App title") + fs.StringVar(&c.Meta.Description, "meta.description", c.Meta.Description, "App description") + fs.StringVar(&c.Profile.Username, "profile.username", c.Profile.Username, "Username for index page") + fs.StringVar(&c.Profile.Email, "profile.email", c.Profile.Email, "Email for index page") + fs.Func("profile.links", "Link(s) for index page", func(s string) error { + parts := strings.SplitN(s, ",", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid profile link %q", s) + } + c.Profile.Links = append(c.Profile.Links, profileLink{ + Name: parts[0], + URL: parts[1], + }) + return nil + }) + + parser := ffjsonnet.ParseConfig{ + Schema: schema, + } + return c, ff.Parse(fs, args, + ff.WithEnvVarPrefix("UGIT"), + ff.WithConfigFileFlag("config"), + ff.WithAllowMissingConfigFile(true), + ff.WithConfigFileParser(parser.Parse), + ) } diff --git a/cmd/ugitd/schema.cue b/cmd/ugitd/schema.cue index b4d1ebe04e7adb735845d7664fc4d0dec7e7d2b0..f6549374d238d9f2d57470c4cf27c2cfb7a62e7f 100644 --- a/cmd/ugitd/schema.cue +++ b/cmd/ugitd/schema.cue @@ -1,68 +1,45 @@ -package ugit - -#Port: int & >0 & <65536 @nix(type=types.port) -#Path: string & !="" -#URL: string & =~"^[a-z][a-z0-9+.-]*://" - -#ProfileLink: string & =~"^[^,]+,.+$" @go(type=profileLink, parse=parseProfileLink) @nix(type=gen.profileLink, apply=gen.profileLinkApply, import=gen) - -// Path to directory containing repositories -"repo-dir": #Path | *".ugit" @nix(default="/var/lib/ugit/repos") - -ssh: { - // Enable SSH server - enable: bool | *true +#Port: int & >0 & <65536 - // Path to authorized_keys - "authorized-keys": #Path | *".ssh/authorized_keys" @nix(default="/var/lib/ugit/authorized_keys") - - // SSH clone URL base - "clone-url": #URL | *"ssh://localhost:8448" - - // SSH port - port: #Port | *8448 +#LogLevel: "debug" | "info" | "warn" | "warning" | "error" - // SSH host key (created if it doesn't exist) - "host-key": #Path | *".ssh/ugit_ed25519" @nix(default="/var/lib/ugit/ugit_ed25519") +#SSHArgs: { + enable?: bool + "authorized-keys"?: string + "clone-url"?: string + port?: #Port + "host-key"?: string } -http: { - // Enable HTTP server - enable: bool | *true +#HTTPArgs: { + enable?: bool + "clone-url"?: string + port?: #Port +} - // HTTP clone URL base - "clone-url": #URL | *"http://localhost:8449" - - // HTTP port - port: #Port | *8449 +#MetaArgs: { + title?: string + description?: string } -meta: { - // App title - title: string | *"ugit" - - // App description - description: string | *"Minimal git server" +#ProfileArgs: { + username?: string + email?: string + links?: [...string] } -profile: { - // Username for index page - username: string | *"" - - // Email for index page - email: string | *"" - - // Link(s) for index page - links: [...#ProfileLink] +#LogArgs: { + level?: #LogLevel + json?: bool } -log: { - // Logging level - level: "debug" | "info" | "warn" | "warning" | *"error" @go(type=slog.Level, parse=parseLogLevel, import="log/slog") - - // Print logs in JSON(L) format - json: bool | *false +#Schema: { + "repo-dir"?: string + "show-private"?: bool + ssh?: #SSHArgs + http?: #HTTPArgs + meta?: #MetaArgs + profile?: #ProfileArgs + log?: #LogArgs } -// Show private repos in web interface -"show-private": bool | *false +#Schema diff --git a/nix/gen.nix b/nix/gen.nix deleted file mode 100644 index aaf22516161881513dc568c23a938b6bf1a0bd29..0000000000000000000000000000000000000000 --- a/nix/gen.nix +++ /dev/null @@ -1,16 +0,0 @@ -{ lib }: -{ - profileLink = lib.types.submodule { - options = { - name = lib.mkOption { - type = lib.types.str; - description = "Display name for the link"; - }; - url = lib.mkOption { - type = lib.types.str; - description = "URL for the link"; - }; - }; - }; - profileLinkApply = link: "${link.name},${link.url}"; -} diff --git a/nix/module.nix b/nix/module.nix index ca0c8ac1375bd7667ea5c2b086d266ffdf7111b6..a40412eae67946fc8ab774a472deee7b803321c2 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -9,6 +9,18 @@ cfg = config.services.ugit; pkg = pkgs.callPackage ./pkg.nix { inherit pkgs; }; jsonFormat = pkgs.formats.json { }; + profileLinkOption = { + options = { + name = lib.mkOption { + type = lib.types.str; + description = "Display name for the link"; + }; + url = lib.mkOption { + type = lib.types.str; + description = "URL for the link"; + }; + }; + }; hookOption = { options = { name = lib.mkOption { @@ -38,6 +50,36 @@ description = "ugit package to use"; default = pkg; }; + homeDir = mkOption { + type = types.str; + description = "ugit home directory"; + default = baseDir; + }; + + repoDir = mkOption { + type = types.str; + description = "where ugit stores repositories"; + default = "${baseDir}/repos"; + }; + + authorizedKeys = mkOption { + type = types.listOf types.str; + description = "list of keys to use for authorized_keys"; + default = [ ]; + }; + + authorizedKeysFile = mkOption { + type = types.str; + description = "path to authorized_keys file ugit uses for auth"; + default = "${baseDir}/authorized_keys"; + }; + + hostKeyFile = mkOption { + type = types.str; + description = "path to host key file (will be created if it doesn't exist)"; + default = "${baseDir}/ugit_ed25519"; + }; + user = mkOption { type = types.str; default = "ugit-${name}"; @@ -56,51 +98,139 @@ description = "A list of pre-receive hooks to run"; default = [ ]; }; - homeDir = mkOption { - type = types.str; - description = "ugit home directory"; - default = baseDir; + showPrivate = mkOption { + type = types.bool; + default = false; + description = "Show private repos in web interface"; }; - repoDir = mkOption { - type = types.str; - description = "where ugit stores repositories"; - default = "${baseDir}/repos"; + ssh = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable SSH server"; + }; + + cloneUrl = mkOption { + type = types.str; + default = "ssh://localhost:8448"; + description = "SSH clone URL base, e.g. ssh://host:port"; + }; + + port = mkOption { + type = types.port; + default = 8448; + description = "SSH port"; + }; }; - authorizedKeys = mkOption { - type = types.listOf types.str; - description = "list of keys to use for authorized_keys"; - default = [ ]; + http = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable HTTP server"; + }; + + cloneUrl = mkOption { + type = types.str; + default = "http://localhost:8449"; + description = "HTTP clone URL base, e.g. http://host:port"; + }; + + port = mkOption { + type = types.port; + default = 8449; + description = "HTTP port"; + }; }; - hostKeyFile = mkOption { - type = types.str; - description = "path to host key file (will be created if it doesn't exist)"; - default = "${baseDir}/ugit_ed25519"; + meta = { + title = mkOption { + type = types.str; + default = "ugit"; + description = "App title"; + }; + + description = mkOption { + type = types.str; + default = "Minimal git server"; + description = "App description"; + }; }; - settings = mkOption { - type = types.submodule { - options = import ./options.gen.nix { - inherit lib; - gen = import ./gen.nix { inherit lib; }; - }; + profile = { + username = mkOption { + type = types.str; + default = ""; + description = "Username for index page"; + }; + + email = mkOption { + type = types.str; + default = ""; + description = "Email for index page"; }; + + links = mkOption { + type = types.listOf (types.submodule profileLinkOption); + default = [ ]; + description = "Link(s) for index page"; + }; + }; + + log = { + level = mkOption { + type = types.enum [ + "debug" + "info" + "warn" + "warning" + "error" + ]; + default = "error"; + description = "Logging level"; + }; + + json = mkOption { + type = types.bool; + default = false; + description = "Print logs in JSON(L) format"; + }; + }; + + extraConfig = mkOption { + type = types.attrs; default = { }; - description = "ugit configuration, rendered to JSON and passed via --config."; + description = "Extra config.jsonnet fields, merged over the generated config"; }; }; }; mkConfig = cfg: authorizedKeysPath: - lib.recursiveUpdate cfg.settings { + lib.recursiveUpdate { + "repo-dir" = cfg.repoDir; + "show-private" = cfg.showPrivate; ssh = { - authorized-keys = authorizedKeysPath; - host-key = cfg.hostKeyFile; + inherit (cfg.ssh) enable port; + "clone-url" = cfg.ssh.cloneUrl; + "authorized-keys" = authorizedKeysPath; + "host-key" = cfg.hostKeyFile; + }; + http = { + inherit (cfg.http) enable port; + "clone-url" = cfg.http.cloneUrl; }; - repo-dir = cfg.repoDir; - }; + meta = { + inherit (cfg.meta) title description; + }; + profile = { + inherit (cfg.profile) username email; + links = map (l: "${l.name},${l.url}") cfg.profile.links; + }; + log = { + inherit (cfg.log) level json; + }; + } cfg.extraConfig; in { options = { @@ -198,10 +328,10 @@ authorizedKeysPath = if (builtins.length instanceCfg.authorizedKeys) > 0 then authorizedKeysFile else - instanceCfg.settings.authorized-keys; - configFile = pkgs.writeText "ugit-${name}.json" ( + instanceCfg.authorizedKeysFile; + configFile = pkgs.writeText "ugit-${name}.jsonnet" ( builtins.readFile ( - jsonFormat.generate "ugit-${name}-json" (mkConfig instanceCfg authorizedKeysPath) + jsonFormat.generate "ugit-${name}-jsonnet" (mkConfig instanceCfg authorizedKeysPath) ) ); in diff --git a/nix/options.gen.nix b/nix/options.gen.nix deleted file mode 100644 index cc9fd4496693d602ead5b7be9c8bc8dfbae10b86..0000000000000000000000000000000000000000 --- a/nix/options.gen.nix +++ /dev/null @@ -1,109 +0,0 @@ -# Code generated by gen from schema.cue; DO NOT EDIT. -{ lib, gen }: -let - inherit (lib) mkOption types; -in -{ - repo-dir = mkOption { - type = types.nonEmptyStr; - default = "/var/lib/ugit/repos"; - description = "Path to directory containing repositories"; - }; - - ssh = { - enable = mkOption { - type = types.bool; - default = true; - description = "Enable SSH server"; - }; - authorized-keys = mkOption { - type = types.nonEmptyStr; - default = "/var/lib/ugit/authorized_keys"; - description = "Path to authorized_keys"; - }; - clone-url = mkOption { - type = types.strMatching "[a-z][a-z0-9+.-]*://.*"; - default = "ssh://localhost:8448"; - description = "SSH clone URL base"; - }; - port = mkOption { - type = types.port; - default = 8448; - description = "SSH port"; - }; - host-key = mkOption { - type = types.nonEmptyStr; - default = "/var/lib/ugit/ugit_ed25519"; - description = "SSH host key (created if it doesn't exist)"; - }; - }; - - http = { - enable = mkOption { - type = types.bool; - default = true; - description = "Enable HTTP server"; - }; - clone-url = mkOption { - type = types.strMatching "[a-z][a-z0-9+.-]*://.*"; - default = "http://localhost:8449"; - description = "HTTP clone URL base"; - }; - port = mkOption { - type = types.port; - default = 8449; - description = "HTTP port"; - }; - }; - - meta = { - title = mkOption { - type = types.str; - default = "ugit"; - description = "App title"; - }; - description = mkOption { - type = types.str; - default = "Minimal git server"; - description = "App description"; - }; - }; - - profile = { - username = mkOption { - type = types.str; - default = ""; - description = "Username for index page"; - }; - email = mkOption { - type = types.str; - default = ""; - description = "Email for index page"; - }; - links = mkOption { - type = types.listOf gen.profileLink; - default = [ ]; - apply = map gen.profileLinkApply; - description = "Link(s) for index page"; - }; - }; - - log = { - level = mkOption { - type = types.enum [ "debug" "info" "warn" "warning" "error" ]; - default = "error"; - description = "Logging level"; - }; - json = mkOption { - type = types.bool; - default = false; - description = "Print logs in JSON(L) format"; - }; - }; - - show-private = mkOption { - type = types.bool; - default = false; - description = "Show private repos in web interface"; - }; -} diff --git a/nix/vm.nix b/nix/vm.nix index c04f7cecf88d7a84e58020eb3391a279f74d43bf..5c9d4d5f3be1ed485c9fb1398490244436e6e90f 100644 --- a/nix/vm.nix +++ b/nix/vm.nix @@ -27,18 +27,10 @@ services.openssh.enable = true; services.ugit.vm = { enable = true; authorizedKeys = [ pubKey ]; - settings = { - show-private = true; - log = { - level = "debug"; - json = true; - }; - profile.links = [ - { - name = "Example"; - url = "https://example.com"; - } - ]; + showPrivate = true; + log = { + level = "debug"; + json = true; }; hooks = [ {