From b6ae95f55ba134f5ef124d377ed3ad0a556b8cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Thu, 2 May 2024 20:19:25 +0200 Subject: [PATCH] feat(cli): upload command --- cmd/hcloud-image/cmd/cleanup.go | 40 ++++++ cmd/hcloud-image/cmd/list.go | 40 ++++++ cmd/hcloud-image/cmd/root.go | 76 +++++++++++ cmd/hcloud-image/cmd/upload.go | 83 ++++++++++++ cmd/hcloud-image/go.mod | 23 ++++ cmd/hcloud-image/go.sum | 26 ++++ cmd/hcloud-image/main.go | 17 +++ cmd/hcloud-image/util/ui/slog_handler.go | 155 +++++++++++++++++++++++ go.work | 6 + go.work.sum | 50 ++++++++ 10 files changed, 516 insertions(+) create mode 100644 cmd/hcloud-image/cmd/cleanup.go create mode 100644 cmd/hcloud-image/cmd/list.go create mode 100644 cmd/hcloud-image/cmd/root.go create mode 100644 cmd/hcloud-image/cmd/upload.go create mode 100644 cmd/hcloud-image/go.mod create mode 100644 cmd/hcloud-image/go.sum create mode 100644 cmd/hcloud-image/main.go create mode 100644 cmd/hcloud-image/util/ui/slog_handler.go create mode 100644 go.work create mode 100644 go.work.sum diff --git a/cmd/hcloud-image/cmd/cleanup.go b/cmd/hcloud-image/cmd/cleanup.go new file mode 100644 index 0000000..b11b085 --- /dev/null +++ b/cmd/hcloud-image/cmd/cleanup.go @@ -0,0 +1,40 @@ +/* +Copyright © 2024 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// cleanupCmd represents the cleanup command +var cleanupCmd = &cobra.Command{ + Use: "cleanup", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("cleanup called") + }, +} + +func init() { + rootCmd.AddCommand(cleanupCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // cleanupCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // cleanupCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/hcloud-image/cmd/list.go b/cmd/hcloud-image/cmd/list.go new file mode 100644 index 0000000..b8e09c2 --- /dev/null +++ b/cmd/hcloud-image/cmd/list.go @@ -0,0 +1,40 @@ +/* +Copyright © 2024 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// listCmd represents the list command +var listCmd = &cobra.Command{ + Use: "list", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("list called") + }, +} + +func init() { + rootCmd.AddCommand(listCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // listCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // listCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/hcloud-image/cmd/root.go b/cmd/hcloud-image/cmd/root.go new file mode 100644 index 0000000..cab39b1 --- /dev/null +++ b/cmd/hcloud-image/cmd/root.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "context" + "fmt" + "log/slog" + "os" + "time" + + "github.com/hetznercloud/hcloud-go/v2/hcloud" + "github.com/spf13/cobra" + + hcloud_upload_image "github.com/apricote/hcloud-upload-image" + "github.com/apricote/hcloud-upload-image/util/contextlogger" + "github.com/apricote/hcloud-upload-image/util/control" +) + +// The pre-authenticated client. Set in the root command PersistentPreRun +var client hcloud_upload_image.SnapshotClient + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "hcloud-image", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, + + PersistentPreRun: func(cmd *cobra.Command, _ []string) { + ctx := cmd.Context() + + // Add logger to command context + logger := slog.Default() + ctx = contextlogger.New(ctx, logger) + cmd.SetContext(ctx) + + client = newClient(ctx) + }, +} + +func newClient(ctx context.Context) hcloud_upload_image.SnapshotClient { + logger := contextlogger.From(ctx) + // Build hcloud-go client + if os.Getenv("HCLOUD_TOKEN") == "" { + logger.ErrorContext(ctx, "You need to set the HCLOUD_TOKEN environment variable to your Hetzner Cloud API Token.") + os.Exit(1) + } + hcloudClient := hcloud.NewClient( + hcloud.WithToken(os.Getenv("HCLOUD_TOKEN")), + hcloud.WithApplication("hcloud-image", ""), + hcloud.WithPollBackoffFunc(control.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second)), + // hcloud.WithDebugWriter(os.Stderr), + ) + + return hcloud_upload_image.New(hcloudClient) +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.SetErrPrefix("\033[1;31mError:") + rootCmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error { + return fmt.Errorf("fooo") + }) +} diff --git a/cmd/hcloud-image/cmd/upload.go b/cmd/hcloud-image/cmd/upload.go new file mode 100644 index 0000000..81ce5cf --- /dev/null +++ b/cmd/hcloud-image/cmd/upload.go @@ -0,0 +1,83 @@ +package cmd + +import ( + "fmt" + "net/url" + + "github.com/hetznercloud/hcloud-go/v2/hcloud" + "github.com/spf13/cobra" + + hcloud_upload_image "github.com/apricote/hcloud-upload-image" + "github.com/apricote/hcloud-upload-image/util/contextlogger" +) + +const ( + uploadFlagImageURL = "image-url" + uploadFlagCompression = "compression" + uploadFlagArchitecture = "architecture" + uploadFlagDescription = "description" + uploadFlagLabels = "labels" +) + +// uploadCmd represents the upload command +var uploadCmd = &cobra.Command{ + Use: "upload", + Short: "Upload the specified disk image into your Hetzner Cloud project.", + Long: `This command implements a fake "upload", by going through a real server and snapshots. +This does cost a bit of money for the server.`, + + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + logger := contextlogger.From(ctx) + + imageURLString, _ := cmd.Flags().GetString(uploadFlagImageURL) + imageCompression, _ := cmd.Flags().GetString(uploadFlagCompression) + architecture, _ := cmd.Flags().GetString(uploadFlagArchitecture) + description, _ := cmd.Flags().GetString(uploadFlagDescription) + labels, _ := cmd.Flags().GetStringToString(uploadFlagLabels) + + imageURL, err := url.Parse(imageURLString) + if err != nil { + return fmt.Errorf("unable to parse url from --%s=%q: %w", uploadFlagImageURL, imageURLString, err) + } + + image, err := client.Upload(ctx, hcloud_upload_image.UploadOptions{ + ImageURL: imageURL, + ImageCompression: hcloud_upload_image.Compression(imageCompression), + Architecture: hcloud.Architecture(architecture), + Description: hcloud.Ptr(description), + Labels: labels, + }) + if err != nil { + return fmt.Errorf("failed to upload the image: %w", err) + } + + logger.InfoContext(ctx, "Successfully uploaded the image!", "image", image.ID) + + return nil + }, +} + +func init() { + rootCmd.AddCommand(uploadCmd) + + uploadCmd.Flags().String(uploadFlagImageURL, "", "Remote URL of the disk image that should be uploaded (required)") + _ = uploadCmd.MarkFlagRequired(uploadFlagImageURL) + + uploadCmd.Flags().String(uploadFlagCompression, "", "Type of compression that was used on the disk image") + _ = uploadCmd.RegisterFlagCompletionFunc( + uploadFlagCompression, + cobra.FixedCompletions([]string{string(hcloud_upload_image.CompressionBZ2)}, cobra.ShellCompDirectiveNoFileComp), + ) + + uploadCmd.Flags().String(uploadFlagArchitecture, "", "CPU Architecture of the disk image. Choices: x86|arm") + _ = uploadCmd.RegisterFlagCompletionFunc( + uploadFlagArchitecture, + cobra.FixedCompletions([]string{string(hcloud.ArchitectureX86), string(hcloud.ArchitectureARM)}, cobra.ShellCompDirectiveNoFileComp), + ) + _ = uploadCmd.MarkFlagRequired(uploadFlagArchitecture) + + uploadCmd.Flags().String(uploadFlagDescription, "", "Description for the resulting Image") + + uploadCmd.Flags().StringToString(uploadFlagLabels, map[string]string{}, "Labels for the resulting Image") +} diff --git a/cmd/hcloud-image/go.mod b/cmd/hcloud-image/go.mod new file mode 100644 index 0000000..54b0261 --- /dev/null +++ b/cmd/hcloud-image/go.mod @@ -0,0 +1,23 @@ +module github.com/apricote/hcloud-upload-image/cmd/hcloud-image + +go 1.22.2 + +require ( + github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f + github.com/spf13/cobra v1.8.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect +) diff --git a/cmd/hcloud-image/go.sum b/cmd/hcloud-image/go.sum new file mode 100644 index 0000000..ba0f2a4 --- /dev/null +++ b/cmd/hcloud-image/go.sum @@ -0,0 +1,26 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f h1:c1ahn6OKXkSqwOfCoqyFrjVh14BEC9rD3ok0dehbCno= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/hcloud-image/main.go b/cmd/hcloud-image/main.go new file mode 100644 index 0000000..25af65b --- /dev/null +++ b/cmd/hcloud-image/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "log/slog" + "os" + + "github.com/apricote/hcloud-upload-image/cmd/hcloud-image/cmd" + "github.com/apricote/hcloud-upload-image/cmd/hcloud-image/util/ui" +) + +func init() { + slog.SetDefault(slog.New(ui.NewHandler(os.Stdout, &ui.HandlerOptions{Level: slog.LevelDebug}))) +} + +func main() { + cmd.Execute() +} diff --git a/cmd/hcloud-image/util/ui/slog_handler.go b/cmd/hcloud-image/util/ui/slog_handler.go new file mode 100644 index 0000000..d688d3d --- /dev/null +++ b/cmd/hcloud-image/util/ui/slog_handler.go @@ -0,0 +1,155 @@ +package ui + +import ( + "context" + "fmt" + "io" + "log/slog" + "sync" +) + +const ( + ansiClear = "\033[0m" + ansiBold = "\033[1m" + ansiBoldYellow = "\033[1;93m" + ansiBoldRed = "\033[1;31m" + ansiThinGray = "\033[2;37m" +) + +type Handler struct { + opts HandlerOptions + goas []groupOrAttrs + mu *sync.Mutex + out io.Writer +} + +type HandlerOptions struct { + Level slog.Leveler +} + +// groupOrAttrs holds either a group name or a list of [slog.Attr]. +type groupOrAttrs struct { + group string // group name if non-empty + attrs []slog.Attr // attrs if non-empty +} + +var _ slog.Handler = &Handler{} + +func NewHandler(out io.Writer, opts *HandlerOptions) *Handler { + h := &Handler{ + out: out, + mu: &sync.Mutex{}, + } + if opts != nil { + h.opts = *opts + } + if h.opts.Level == nil { + h.opts.Level = slog.LevelInfo + } + return h +} + +func (h *Handler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.opts.Level.Level() +} + +func (h *Handler) Handle(_ context.Context, record slog.Record) error { + buf := make([]byte, 0, 512) + + formattingPrefix := "" + + switch record.Level { + case slog.LevelInfo: + formattingPrefix = ansiBold + case slog.LevelWarn: + // Bold + Yellow + formattingPrefix = ansiBoldYellow + case slog.LevelError: + // Bold + Red + formattingPrefix = ansiBoldRed + } + + // Print main message in formatted text + buf = fmt.Appendf(buf, "%s%s%s", formattingPrefix, record.Message, ansiClear) + + // Add attributes in thin gray + buf = fmt.Append(buf, ansiThinGray) + + // Attributes from [WithGroup] and [WithAttrs] calls + goas := h.goas + if record.NumAttrs() == 0 { + for len(goas) > 0 && goas[len(goas)-1].group != "" { + goas = goas[:len(goas)-1] + } + } + group := "" + for _, goa := range goas { + if goa.group != "" { + group = goa.group + } else { + for _, a := range goa.attrs { + buf = h.appendAttr(buf, group, a) + } + } + } + + record.Attrs(func(a slog.Attr) bool { + buf = h.appendAttr(buf, group, a) + return true + }) + + buf = fmt.Appendf(buf, "%s\n", ansiClear) + + h.mu.Lock() + defer h.mu.Unlock() + _, err := h.out.Write(buf) + return err +} + +func (h *Handler) appendAttr(buf []byte, group string, a slog.Attr) []byte { + a.Value = a.Value.Resolve() + if a.Equal(slog.Attr{}) { + return buf + } + + if group != "" { + group += "." + } + + switch a.Value.Kind() { + case slog.KindString: + buf = fmt.Appendf(buf, " %s%s=%q", group, a.Key, a.Value) + case slog.KindAny: + if err, ok := a.Value.Any().(error); ok { + buf = fmt.Appendf(buf, " %s%s=%q", group, a.Key, err.Error()) + } else { + buf = fmt.Appendf(buf, " %s%s=%s", group, a.Key, a.Value) + } + default: + buf = fmt.Appendf(buf, " %s%s=%s", group, a.Key, a.Value) + } + + return buf +} + +func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) +} + +func (h *Handler) WithGroup(name string) slog.Handler { + if name == "" { + return h + } + return h.withGroupOrAttrs(groupOrAttrs{group: name}) +} + +func (h *Handler) withGroupOrAttrs(goa groupOrAttrs) *Handler { + h2 := *h + h2.goas = make([]groupOrAttrs, len(h.goas)+1) + copy(h2.goas, h.goas) + h2.goas[len(h2.goas)-1] = goa + return &h2 +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..f8e580f --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.22.2 + +use ( + . + ./cmd/hcloud-image +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..dc1f0c7 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,50 @@ +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/dave/jennifer v1.6.0 h1:MQ/6emI2xM7wt0tJzJzyUik2Q3Tcn2eE0vtYgh4GPVI= +github.com/dave/jennifer v1.6.0/go.mod h1:AxTG893FiZKqxy3FP1kL80VMshSMuz2G+EgvszgGRnk= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20 h1:dAOsPLhnBzIyxu0VvmnKjlNcIlgMK+erD6VRHDtweMI= +github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmattheis/goverter v1.4.0 h1:SrboBYMpGkj1XSgFhWwqzdP024zIa1+58YzUm+0jcBE= +github.com/jmattheis/goverter v1.4.0/go.mod h1:iVIl/4qItWjWj2g3vjouGoYensJbRqDHpzlEVMHHFeY= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/vburenin/ifacemaker v1.2.1 h1:3Vq8B/bfBgjWTkv+jDg4dVL1KHt3k1K4lO7XRxYA2sk= +github.com/vburenin/ifacemaker v1.2.1/go.mod h1:5WqrzX2aD7/hi+okBjcaEQJMg4lDGrpuEX3B8L4Wgrs= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=