@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 10, 23:45 UTC

master
go 609 lines 22.6 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddycmd
16
17 import (
18 "flag"
19 "fmt"
20 "os"
21 "regexp"
22 "strings"
23 "sync"
24
25 "github.com/spf13/cobra"
26 "github.com/spf13/cobra/doc"
27
28 "github.com/caddyserver/caddy/v2"
29 )
30
31 // Command represents a subcommand. Name, Func,
32 // and Short are required.
33 type Command struct {
34 // The name of the subcommand. Must conform to the
35 // format described by the RegisterCommand() godoc.
36 // Required.
37 Name string
38
39 // Usage is a brief message describing the syntax of
40 // the subcommand's flags and args. Use [] to indicate
41 // optional parameters and <> to enclose literal values
42 // intended to be replaced by the user. Do not prefix
43 // the string with "caddy" or the name of the command
44 // since these will be prepended for you; only include
45 // the actual parameters for this command.
46 Usage string
47
48 // Short is a one-line message explaining what the
49 // command does. Should not end with punctuation.
50 // Required.
51 Short string
52
53 // Long is the full help text shown to the user.
54 // Will be trimmed of whitespace on both ends before
55 // being printed.
56 Long string
57
58 // Flags is the flagset for command.
59 // This is ignored if CobraFunc is set.
60 Flags *flag.FlagSet
61
62 // Func is a function that executes a subcommand using
63 // the parsed flags. It returns an exit code and any
64 // associated error.
65 // Required if CobraFunc is not set.
66 Func CommandFunc
67
68 // CobraFunc allows further configuration of the command
69 // via cobra's APIs. If this is set, then Func and Flags
70 // are ignored, with the assumption that they are set in
71 // this function. A caddycmd.WrapCommandFuncForCobra helper
72 // exists to simplify porting CommandFunc to Cobra's RunE.
73 CobraFunc func(*cobra.Command)
74 }
75
76 // CommandFunc is a command's function. It runs the
77 // command and returns the proper exit code along with
78 // any error that occurred.
79 type CommandFunc func(Flags) (int, error)
80
81 // Commands returns a list of commands initialised by
82 // RegisterCommand
83 func Commands() map[string]Command {
84 commandsMu.RLock()
85 defer commandsMu.RUnlock()
86
87 return commands
88 }
89
90 var (
91 commandsMu sync.RWMutex
92 commands = make(map[string]Command)
93 )
94
95 func init() {
96 RegisterCommand(Command{
97 Name: "start",
98 Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--watch] [--pidfile <file>]",
99 Short: "Starts the Caddy process in the background and then returns",
100 Long: `
101 Starts the Caddy process, optionally bootstrapped with an initial config file.
102 This command unblocks after the server starts running or fails to run.
103
104 If --envfile is specified, an environment file with environment variables
105 in the KEY=VALUE format will be loaded into the Caddy process.
106
107 On Windows, the spawned child process will remain attached to the terminal, so
108 closing the window will forcefully stop Caddy; to avoid forgetting this, try
109 using 'caddy run' instead to keep it in the foreground.
110 `,
111 CobraFunc: func(cmd *cobra.Command) {
112 cmd.Flags().StringP("config", "c", "", "Configuration file")
113 cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
114 cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
115 cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically")
116 cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
117 cmd.RunE = WrapCommandFuncForCobra(cmdStart)
118 },
119 })
120
121 RegisterCommand(Command{
122 Name: "run",
123 Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <file>]",
124 Short: `Starts the Caddy process and blocks indefinitely`,
125 Long: `
126 Starts the Caddy process, optionally bootstrapped with an initial config file,
127 and blocks indefinitely until the server is stopped; i.e. runs Caddy in
128 "daemon" mode (foreground).
129
130 If a config file is specified, it will be applied immediately after the process
131 is running. If the config file is not in Caddy's native JSON format, you can
132 specify an adapter with --adapter to adapt the given config file to
133 Caddy's native format. The config adapter must be a registered module. Any
134 warnings will be printed to the log, but beware that any adaptation without
135 errors will immediately be used. If you want to review the results of the
136 adaptation first, use the 'adapt' subcommand.
137
138 As a special case, if the current working directory has a file called
139 "Caddyfile" and the caddyfile config adapter is plugged in (default), then
140 that file will be loaded and used to configure Caddy, even without any command
141 line flags.
142
143 If --envfile is specified, an environment file with environment variables
144 in the KEY=VALUE format will be loaded into the Caddy process.
145
146 If --environ is specified, the environment as seen by the Caddy process will
147 be printed before starting. This is the same as the environ command but does
148 not quit after printing, and can be useful for troubleshooting.
149
150 The --resume flag will override the --config flag if there is a config auto-
151 save file. It is not an error if --resume is used and no autosave file exists.
152
153 If --watch is specified, the config file will be loaded automatically after
154 changes. ⚠️ This can make unintentional config changes easier; only use this
155 option in a local development environment.
156 `,
157 CobraFunc: func(cmd *cobra.Command) {
158 cmd.Flags().StringP("config", "c", "", "Configuration file")
159 cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
160 cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
161 cmd.Flags().BoolP("environ", "e", false, "Print environment")
162 cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)")
163 cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically")
164 cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
165 cmd.Flags().StringP("pingback", "", "", "Echo confirmation bytes to this address on success")
166 cmd.RunE = WrapCommandFuncForCobra(cmdRun)
167 },
168 })
169
170 RegisterCommand(Command{
171 Name: "stop",
172 Usage: "[--config <path> [--adapter <name>]] [--address <interface>]",
173 Short: "Gracefully stops a started Caddy process",
174 Long: `
175 Stops the background Caddy process as gracefully as possible.
176
177 It requires that the admin API is enabled and accessible, since it will
178 use the API's /stop endpoint. The address of this request can be customized
179 using the --address flag, or from the given --config, if not the default.
180 `,
181 CobraFunc: func(cmd *cobra.Command) {
182 cmd.Flags().StringP("config", "c", "", "Configuration file to use to parse the admin address, if --address is not used")
183 cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (when --config is used)")
184 cmd.Flags().StringP("address", "", "", "The address to use to reach the admin API endpoint, if not the default")
185 cmd.RunE = WrapCommandFuncForCobra(cmdStop)
186 },
187 })
188
189 RegisterCommand(Command{
190 Name: "reload",
191 Usage: "--config <path> [--adapter <name>] [--address <interface>]",
192 Short: "Changes the config of the running Caddy instance",
193 Long: `
194 Gives the running Caddy instance a new configuration. This has the same effect
195 as POSTing a document to the /load API endpoint, but is convenient for simple
196 workflows revolving around config files.
197
198 Since the admin endpoint is configurable, the endpoint configuration is loaded
199 from the --address flag if specified; otherwise it is loaded from the given
200 config file; otherwise the default is assumed.
201 `,
202 CobraFunc: func(cmd *cobra.Command) {
203 cmd.Flags().StringP("config", "c", "", "Configuration file (required)")
204 cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
205 cmd.Flags().StringP("address", "", "", "Address of the administration listener, if different from config")
206 cmd.Flags().BoolP("force", "f", false, "Force config reload, even if it is the same")
207 cmd.RunE = WrapCommandFuncForCobra(cmdReload)
208 },
209 })
210
211 RegisterCommand(Command{
212 Name: "version",
213 Short: "Prints the version",
214 Long: `
215 Prints the version of this Caddy binary.
216
217 Version information must be embedded into the binary at compile-time in
218 order for Caddy to display anything useful with this command. If Caddy
219 is built from within a version control repository, the Go command will
220 embed the revision hash if available. However, if Caddy is built in the
221 way specified by our online documentation (or by using xcaddy), more
222 detailed version information is printed as given by Go modules.
223
224 For more details about the full version string, see the Go module
225 documentation: https://go.dev/doc/modules/version-numbers
226 `,
227 Func: cmdVersion,
228 })
229
230 RegisterCommand(Command{
231 Name: "list-modules",
232 Usage: "[--packages] [--versions] [--skip-standard] [--json]",
233 Short: "Lists the installed Caddy modules",
234 CobraFunc: func(cmd *cobra.Command) {
235 cmd.Flags().BoolP("packages", "", false, "Print package paths")
236 cmd.Flags().BoolP("versions", "", false, "Print version information")
237 cmd.Flags().BoolP("skip-standard", "s", false, "Skip printing standard modules")
238 cmd.Flags().BoolP("json", "", false, "Print modules in JSON format")
239 cmd.RunE = WrapCommandFuncForCobra(cmdListModules)
240 },
241 })
242
243 RegisterCommand(Command{
244 Name: "build-info",
245 Short: "Prints information about this build",
246 Func: cmdBuildInfo,
247 })
248
249 RegisterCommand(Command{
250 Name: "environ",
251 Usage: "[--envfile <path>]",
252 Short: "Prints the environment",
253 Long: `
254 Prints the environment as seen by this Caddy process.
255
256 The environment includes variables set in the system. If your Caddy
257 configuration uses environment variables (e.g. "{env.VARIABLE}") then
258 this command can be useful for verifying that the variables will have
259 the values you expect in your config.
260
261 If --envfile is specified, an environment file with environment variables
262 in the KEY=VALUE format will be loaded into the Caddy process.
263
264 Note that environments may be different depending on how you run Caddy.
265 Environments for Caddy instances started by service managers such as
266 systemd are often different than the environment inherited from your
267 shell or terminal.
268
269 You can also print the environment the same time you use "caddy run"
270 by adding the "--environ" flag.
271
272 Environments may contain sensitive data.
273 `,
274 CobraFunc: func(cmd *cobra.Command) {
275 cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
276 cmd.RunE = WrapCommandFuncForCobra(cmdEnviron)
277 },
278 })
279
280 RegisterCommand(Command{
281 Name: "adapt",
282 Usage: "--config <path> [--adapter <name>] [--pretty] [--validate] [--envfile <path>]",
283 Short: "Adapts a configuration to Caddy's native JSON",
284 Long: `
285 Adapts a configuration to Caddy's native JSON format and writes the
286 output to stdout, along with any warnings to stderr.
287
288 If --pretty is specified, the output will be formatted with indentation
289 for human readability.
290
291 If --validate is used, the adapted config will be checked for validity.
292 If the config is invalid, an error will be printed to stderr and a non-
293 zero exit status will be returned.
294
295 If --envfile is specified, an environment file with environment variables
296 in the KEY=VALUE format will be loaded into the Caddy process.
297
298 If you wish to use stdin instead of a regular file, use - as the path.
299 `,
300 CobraFunc: func(cmd *cobra.Command) {
301 cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
302 cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter")
303 cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability")
304 cmd.Flags().BoolP("validate", "", false, "Validate the output")
305 cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
306 cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
307 },
308 })
309
310 RegisterCommand(Command{
311 Name: "validate",
312 Usage: "--config <path> [--adapter <name>] [--envfile <path>]",
313 Short: "Tests whether a configuration file is valid",
314 Long: `
315 Loads and provisions the provided config, but does not start running it.
316 This reveals any errors with the configuration through the loading and
317 provisioning stages.
318
319 If --envfile is specified, an environment file with environment variables
320 in the KEY=VALUE format will be loaded into the Caddy process.
321 `,
322 CobraFunc: func(cmd *cobra.Command) {
323 cmd.Flags().StringP("config", "c", "", "Input configuration file")
324 cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
325 cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
326 cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
327 },
328 })
329
330 RegisterCommand(Command{
331 Name: "storage",
332 Short: "Commands for working with Caddy's storage (EXPERIMENTAL)",
333 Long: `
334 Allows exporting and importing Caddy's storage contents. The two commands can be
335 combined in a pipeline to transfer directly from one storage to another:
336
337 $ caddy storage export --config Caddyfile.old --output - |
338 > caddy storage import --config Caddyfile.new --input -
339
340 The - argument refers to stdout and stdin, respectively.
341
342 NOTE: When importing to or exporting from file_system storage (the default), the command
343 should be run as the user that owns the associated root path.
344
345 EXPERIMENTAL: May be changed or removed.
346 `,
347 CobraFunc: func(cmd *cobra.Command) {
348 exportCmd := &cobra.Command{
349 Use: "export --config <path> --output <path>",
350 Short: "Exports storage assets as a tarball",
351 Long: `
352 The contents of the configured storage module (TLS certificates, etc)
353 are exported via a tarball.
354
355 --output is required, - can be given for stdout.
356 `,
357 RunE: WrapCommandFuncForCobra(cmdExportStorage),
358 }
359 exportCmd.Flags().StringP("config", "c", "", "Input configuration file (required)")
360 exportCmd.Flags().StringP("output", "o", "", "Output path")
361 cmd.AddCommand(exportCmd)
362
363 importCmd := &cobra.Command{
364 Use: "import --config <path> --input <path>",
365 Short: "Imports storage assets from a tarball.",
366 Long: `
367 Imports storage assets to the configured storage module. The import file must be
368 a tar archive.
369
370 --input is required, - can be given for stdin.
371 `,
372 RunE: WrapCommandFuncForCobra(cmdImportStorage),
373 }
374 importCmd.Flags().StringP("config", "c", "", "Configuration file to load (required)")
375 importCmd.Flags().StringP("input", "i", "", "Tar of assets to load (required)")
376 cmd.AddCommand(importCmd)
377 },
378 })
379
380 RegisterCommand(Command{
381 Name: "fmt",
382 Usage: "[--overwrite] [--diff] [--imports] [<path>]",
383 Short: "Formats a Caddyfile",
384 Long: `
385 Formats the Caddyfile by adding proper indentation and spaces to improve
386 human readability. It prints the result to stdout.
387
388 If --overwrite is specified, the output will be written to the config file
389 directly instead of printing it.
390
391 If --diff is specified, the output will be compared against the input, and
392 lines will be prefixed with '-' and '+' where they differ. Note that
393 unchanged lines are prefixed with two spaces for alignment, and that this
394 is not a valid patch format.
395
396 If --imports is specified, all files referenced by import directives will
397 also be formatted. Each file's output is preceded by a header line (# <path>)
398 when printing to stdout, so multiple files are distinguishable. The --imports
399 flag cannot be used when reading from stdin.
400
401 If you wish to use stdin instead of a regular file, use - as the path.
402 When reading from stdin, the --overwrite flag has no effect: the result
403 is always printed to stdout.
404 `,
405 CobraFunc: func(cmd *cobra.Command) {
406 cmd.Flags().StringP("config", "c", "", "Configuration file")
407 cmd.Flags().BoolP("overwrite", "w", false, "Overwrite the input file with the results")
408 cmd.Flags().BoolP("diff", "d", false, "Print the differences between the input file and the formatted output")
409 cmd.Flags().Bool("imports", false, "Also format files referenced by import directives")
410 cmd.RunE = WrapCommandFuncForCobra(cmdFmt)
411 },
412 })
413
414 RegisterCommand(Command{
415 Name: "upgrade",
416 Short: "Upgrade Caddy (EXPERIMENTAL)",
417 Long: `
418 Downloads an updated Caddy binary with the same modules/plugins at the
419 latest versions. EXPERIMENTAL: May be changed or removed.
420 `,
421 CobraFunc: func(cmd *cobra.Command) {
422 cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
423 cmd.RunE = WrapCommandFuncForCobra(cmdUpgrade)
424 },
425 })
426
427 RegisterCommand(Command{
428 Name: "add-package",
429 Usage: "<package[@version]...>",
430 Short: "Adds Caddy packages (EXPERIMENTAL)",
431 Long: `
432 Downloads an updated Caddy binary with the specified packages (module/plugin)
433 added, with an optional version specified (e.g., "package@version"). Retains
434 existing packages. Returns an error if any of the specified packages are already
435 included. EXPERIMENTAL: May be changed or removed.
436 `,
437 CobraFunc: func(cmd *cobra.Command) {
438 cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
439 cmd.RunE = WrapCommandFuncForCobra(cmdAddPackage)
440 },
441 })
442
443 RegisterCommand(Command{
444 Name: "remove-package",
445 Func: cmdRemovePackage,
446 Usage: "<packages...>",
447 Short: "Removes Caddy packages (EXPERIMENTAL)",
448 Long: `
449 Downloads an updated Caddy binaries without the specified packages (module/plugin).
450 Returns an error if any of the packages are not included.
451 EXPERIMENTAL: May be changed or removed.
452 `,
453 CobraFunc: func(cmd *cobra.Command) {
454 cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
455 cmd.RunE = WrapCommandFuncForCobra(cmdRemovePackage)
456 },
457 })
458
459 defaultFactory.Use(func(rootCmd *cobra.Command) {
460 manpageCommand := Command{
461 Name: "manpage",
462 Usage: "--directory <path>",
463 Short: "Generates the manual pages for Caddy commands",
464 Long: `
465 Generates the manual pages for Caddy commands into the designated directory
466 tagged into section 8 (System Administration).
467
468 The manual page files are generated into the directory specified by the
469 argument of --directory. If the directory does not exist, it will be created.
470 `,
471 CobraFunc: func(cmd *cobra.Command) {
472 cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
473 cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
474 dir := strings.TrimSpace(fl.String("directory"))
475 if dir == "" {
476 return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
477 }
478 if err := os.MkdirAll(dir, 0o755); err != nil {
479 return caddy.ExitCodeFailedQuit, err
480 }
481 if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
482 Title: "Caddy",
483 Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
484 }, dir); err != nil {
485 return caddy.ExitCodeFailedQuit, err
486 }
487 return caddy.ExitCodeSuccess, nil
488 })
489 },
490 }
491
492 // source: https://github.com/spf13/cobra/blob/6dec1ae26659a130bdb4c985768d1853b0e1bc06/site/content/completions/_index.md
493 completionCommand := Command{
494 Name: "completion",
495 Usage: "[bash|zsh|fish|powershell]",
496 Short: "Generate completion script",
497 Long: fmt.Sprintf(`To load completions:
498
499 Bash:
500
501 $ source <(%[1]s completion bash)
502
503 # To load completions for each session, execute once:
504 # Linux:
505 $ %[1]s completion bash > /etc/bash_completion.d/%[1]s
506 # macOS:
507 $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
508
509 Zsh:
510
511 # If shell completion is not already enabled in your environment,
512 # you will need to enable it. You can execute the following once:
513
514 $ echo "autoload -U compinit; compinit" >> ~/.zshrc
515
516 # To load completions for each session, execute once:
517 $ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
518
519 # You will need to start a new shell for this setup to take effect.
520
521 fish:
522
523 $ %[1]s completion fish | source
524
525 # To load completions for each session, execute once:
526 $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
527
528 PowerShell:
529
530 PS> %[1]s completion powershell | Out-String | Invoke-Expression
531
532 # To load completions for every new session, run:
533 PS> %[1]s completion powershell > %[1]s.ps1
534 # and source this file from your PowerShell profile.
535 `, rootCmd.Root().Name()),
536 CobraFunc: func(cmd *cobra.Command) {
537 cmd.DisableFlagsInUseLine = true
538 cmd.ValidArgs = []string{"bash", "zsh", "fish", "powershell"}
539 cmd.Args = cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)
540 cmd.RunE = func(cmd *cobra.Command, args []string) error {
541 switch args[0] {
542 case "bash":
543 return cmd.Root().GenBashCompletion(os.Stdout)
544 case "zsh":
545 return cmd.Root().GenZshCompletion(os.Stdout)
546 case "fish":
547 return cmd.Root().GenFishCompletion(os.Stdout, true)
548 case "powershell":
549 return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
550 default:
551 return fmt.Errorf("unrecognized shell: %s", args[0])
552 }
553 }
554 },
555 }
556
557 rootCmd.AddCommand(caddyCmdToCobra(manpageCommand))
558 rootCmd.AddCommand(caddyCmdToCobra(completionCommand))
559
560 // add manpage and completion commands to the map of
561 // available commands, because they're not registered
562 // through RegisterCommand.
563 commandsMu.Lock()
564 commands[manpageCommand.Name] = manpageCommand
565 commands[completionCommand.Name] = completionCommand
566 commandsMu.Unlock()
567 })
568 }
569
570 // RegisterCommand registers the command cmd.
571 // cmd.Name must be unique and conform to the
572 // following format:
573 //
574 // - lowercase
575 // - ASCII lowercase letters, digits and hyphens only
576 // - cannot start or end with a hyphen
577 // - hyphen cannot be adjacent to another hyphen
578 //
579 // This function panics if the name is already registered,
580 // if the name does not meet the described format, or if
581 // any of the fields are missing from cmd.
582 //
583 // This function should be used in init().
584 func RegisterCommand(cmd Command) {
585 commandsMu.Lock()
586 defer commandsMu.Unlock()
587
588 if cmd.Name == "" {
589 panic("command name is required")
590 }
591 if cmd.Func == nil && cmd.CobraFunc == nil {
592 panic("command function missing")
593 }
594 if cmd.Short == "" {
595 panic("command short string is required")
596 }
597 if _, exists := commands[cmd.Name]; exists {
598 panic("command already registered: " + cmd.Name)
599 }
600 if !commandNameRegex.MatchString(cmd.Name) {
601 panic("invalid command name")
602 }
603 defaultFactory.Use(func(rootCmd *cobra.Command) {
604 rootCmd.AddCommand(caddyCmdToCobra(cmd))
605 })
606 commands[cmd.Name] = cmd
607 }
608
609 var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)