diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b11a7622..a3a7e67d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,9 @@ "golang.go", "unifiedjs.vscode-mdx", "a-h.templ", - "redhat.vscode-yaml" + "redhat.vscode-yaml", + "hashicorp.hcl", + "fredwangwang.vscode-hcl-format" ] } } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c85abc80..af20e0ed 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -5,6 +5,8 @@ "golang.go", "unifiedjs.vscode-mdx", "a-h.templ", - "redhat.vscode-yaml" + "redhat.vscode-yaml", + "hashicorp.hcl", + "fredwangwang.vscode-hcl-format" ] } \ No newline at end of file diff --git a/cmd/osiris/internal/config/bind.go b/cmd/osiris/internal/config/bind.go new file mode 100644 index 00000000..5467cc62 --- /dev/null +++ b/cmd/osiris/internal/config/bind.go @@ -0,0 +1,48 @@ +package config + +import ( + "errors" + "fmt" + "net" +) + +var ( + ErrCantBindToPort = errors.New("bind: can't bind to host:port") +) + +type Bind struct { + HTTP string `hcl:"http"` + HTTPS string `hcl:"https"` + Metrics string `hcl:"metrics"` +} + +func (b *Bind) Valid() error { + var errs []error + + ln, err := net.Listen("tcp", b.HTTP) + if err != nil { + errs = append(errs, fmt.Errorf("%w %q: %w", ErrCantBindToPort, b.HTTP, err)) + } else { + defer ln.Close() + } + + ln, err = net.Listen("tcp", b.HTTPS) + if err != nil { + errs = append(errs, fmt.Errorf("%w %q: %w", ErrCantBindToPort, b.HTTPS, err)) + } else { + defer ln.Close() + } + + ln, err = net.Listen("tcp", b.Metrics) + if err != nil { + errs = append(errs, fmt.Errorf("%w %q: %w", ErrCantBindToPort, b.Metrics, err)) + } else { + defer ln.Close() + } + + if len(errs) != 0 { + return errors.Join(errs...) + } + + return nil +} diff --git a/cmd/osiris/internal/config/bind_test.go b/cmd/osiris/internal/config/bind_test.go new file mode 100644 index 00000000..56f298be --- /dev/null +++ b/cmd/osiris/internal/config/bind_test.go @@ -0,0 +1,55 @@ +package config + +import ( + "errors" + "net" + "testing" +) + +func TestBindValid(t *testing.T) { + for _, tt := range []struct { + name string + precondition func(t *testing.T) + bind Bind + err error + }{ + { + name: "basic", + precondition: nil, + bind: Bind{ + HTTP: ":8081", + HTTPS: ":8082", + Metrics: ":8083", + }, + err: nil, + }, + { + name: "reused ports", + precondition: func(t *testing.T) { + ln, err := net.Listen("tcp", ":8081") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { ln.Close() }) + }, + bind: Bind{ + HTTP: ":8081", + HTTPS: ":8081", + Metrics: ":8081", + }, + err: ErrCantBindToPort, + }, + } { + t.Run(tt.name, func(t *testing.T) { + if tt.precondition != nil { + tt.precondition(t) + } + + if err := tt.bind.Valid(); !errors.Is(err, tt.err) { + t.Logf("want: %v", tt.err) + t.Logf("got: %v", err) + t.Error("got wrong error from validation function") + } + }) + } +} diff --git a/cmd/osiris/internal/config/config.go b/cmd/osiris/internal/config/config.go new file mode 100644 index 00000000..ae53c047 --- /dev/null +++ b/cmd/osiris/internal/config/config.go @@ -0,0 +1,31 @@ +package config + +import ( + "errors" + "fmt" +) + +type Toplevel struct { + Bind Bind `hcl:"bind,block"` + Domains []Domain `hcl:"domain,block"` +} + +func (t *Toplevel) Valid() error { + var errs []error + + if err := t.Bind.Valid(); err != nil { + errs = append(errs, fmt.Errorf("invalid bind block:\n%w", err)) + } + + for _, d := range t.Domains { + if err := d.Valid(); err != nil { + errs = append(errs, fmt.Errorf("when parsing domain %s: %w", d.Name, err)) + } + } + + if len(errs) != 0 { + return fmt.Errorf("invalid configuration file:\n%w", errors.Join(errs...)) + } + + return nil +} diff --git a/cmd/osiris/internal/config/domain.go b/cmd/osiris/internal/config/domain.go new file mode 100644 index 00000000..1d7336b2 --- /dev/null +++ b/cmd/osiris/internal/config/domain.go @@ -0,0 +1,65 @@ +package config + +import ( + "errors" + "fmt" + "net/url" + + "golang.org/x/net/idna" +) + +var ( + ErrInvalidDomainName = errors.New("domain: name is invalid") + ErrInvalidDomainTLSConfig = errors.New("domain: TLS config is invalid") + ErrInvalidURL = errors.New("invalid URL") + ErrInvalidURLScheme = errors.New("URL has invalid scheme") +) + +type Domain struct { + Name string `hcl:"name,label"` + TLS TLS `hcl:"tls,block"` + Target string `hcl:"target"` + HealthTarget string `hcl:"health_target"` +} + +func (d Domain) Valid() error { + var errs []error + + if _, err := idna.Lookup.ToASCII(d.Name); err != nil { + errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidDomainName, d.Name, err)) + } + + if err := d.TLS.Valid(); err != nil { + errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidDomainTLSConfig, err)) + } + + if err := isURLValid(d.Target); err != nil { + errs = append(errs, fmt.Errorf("target has %w %q: %w", ErrInvalidURL, d.Target, err)) + } + + if err := isURLValid(d.HealthTarget); err != nil { + errs = append(errs, fmt.Errorf("health_target has %w %q: %w", ErrInvalidURL, d.HealthTarget, err)) + } + + if len(errs) != 0 { + return errors.Join(errs...) + } + + return nil +} + +func isURLValid(input string) error { + u, err := url.Parse(input) + if err != nil { + return err + } + + switch u.Scheme { + case "http", "https", "h2c", "unix": + // do nothing + default: + return fmt.Errorf("%w %s has scheme %s (want http, https, h2c, unix)", ErrInvalidURLScheme, input, u.Scheme) + } + + return nil +} diff --git a/cmd/osiris/internal/config/domain_test.go b/cmd/osiris/internal/config/domain_test.go new file mode 100644 index 00000000..ac5bf5f4 --- /dev/null +++ b/cmd/osiris/internal/config/domain_test.go @@ -0,0 +1,89 @@ +package config + +import ( + "errors" + "testing" +) + +func TestDomainValid(t *testing.T) { + for _, tt := range []struct { + name string + input Domain + err error + }{ + { + name: "simple happy path", + input: Domain{ + Name: "anubis.techaro.lol", + TLS: TLS{ + Cert: "./testdata/tls/selfsigned.crt", + Key: "./testdata/tls/selfsigned.key", + }, + Target: "http://localhost:3000", + HealthTarget: "http://localhost:9091/healthz", + }, + }, + { + name: "invalid domain name", + input: Domain{ + Name: "\uFFFD.techaro.lol", + TLS: TLS{ + Cert: "./testdata/tls/selfsigned.crt", + Key: "./testdata/tls/selfsigned.key", + }, + Target: "http://localhost:3000", + HealthTarget: "http://localhost:9091/healthz", + }, + err: ErrInvalidDomainName, + }, + { + name: "invalid tls config", + input: Domain{ + Name: "anubis.techaro.lol", + TLS: TLS{ + Cert: "./testdata/tls/invalid.crt", + Key: "./testdata/tls/invalid.key", + }, + Target: "http://localhost:3000", + HealthTarget: "http://localhost:9091/healthz", + }, + err: ErrInvalidDomainTLSConfig, + }, + { + name: "invalid URL", + input: Domain{ + Name: "anubis.techaro.lol", + TLS: TLS{ + Cert: "./testdata/tls/selfsigned.crt", + Key: "./testdata/tls/selfsigned.key", + }, + Target: "file://[::1:3000", + HealthTarget: "file://[::1:9091/healthz", + }, + err: ErrInvalidURL, + }, + { + name: "wrong URL scheme", + input: Domain{ + Name: "anubis.techaro.lol", + TLS: TLS{ + Cert: "./testdata/tls/selfsigned.crt", + Key: "./testdata/tls/selfsigned.key", + }, + Target: "file://localhost:3000", + HealthTarget: "file://localhost:9091/healthz", + }, + err: ErrInvalidURLScheme, + }, + } { + t.Run(tt.name, func(t *testing.T) { + if err := tt.input.Valid(); !errors.Is(err, tt.err) { + t.Logf("want: %v", tt.err) + t.Logf("got: %v", err) + t.Error("got wrong error from validation function") + } else { + t.Log(err) + } + }) + } +} diff --git a/cmd/osiris/internal/config/testdata/tls/invalid.crt b/cmd/osiris/internal/config/testdata/tls/invalid.crt new file mode 100644 index 00000000..a8de15e5 --- /dev/null +++ b/cmd/osiris/internal/config/testdata/tls/invalid.crt @@ -0,0 +1 @@ +aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar \ No newline at end of file diff --git a/cmd/osiris/internal/config/testdata/tls/invalid.key b/cmd/osiris/internal/config/testdata/tls/invalid.key new file mode 100644 index 00000000..a8de15e5 --- /dev/null +++ b/cmd/osiris/internal/config/testdata/tls/invalid.key @@ -0,0 +1 @@ +aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar \ No newline at end of file diff --git a/cmd/osiris/internal/config/testdata/tls/selfsigned.crt b/cmd/osiris/internal/config/testdata/tls/selfsigned.crt new file mode 100644 index 00000000..7b791ac8 --- /dev/null +++ b/cmd/osiris/internal/config/testdata/tls/selfsigned.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBnzCCAVGgAwIBAgIUAw8funCpiB3ZAAPoWdSCWnzbsFIwBQYDK2VwMEUxCzAJ +BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MTkwMjM1WhcNMjUwODE3MTkwMjM1 +WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY +SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAcXDHXV3vgpvjtTaz +s0Oj/73rMr06bhyGGhleYS1MNoWjUzBRMB0GA1UdDgQWBBQwmfKPthucFHB6Wfgz +2Nj5nkMQOjAfBgNVHSMEGDAWgBQwmfKPthucFHB6Wfgz2Nj5nkMQOjAPBgNVHRMB +Af8EBTADAQH/MAUGAytlcANBALBYbULlGwB7Ro0UTgUoQDNxEvayn3qzVFHIt7lC +/2/NzNBkk4yPT+a4mbRuydxLkv+JIvmQbarZxpksYnWlCAM= +-----END CERTIFICATE----- diff --git a/cmd/osiris/internal/config/testdata/tls/selfsigned.key b/cmd/osiris/internal/config/testdata/tls/selfsigned.key new file mode 100644 index 00000000..f609803f --- /dev/null +++ b/cmd/osiris/internal/config/testdata/tls/selfsigned.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIOHKoX22Mha6SnnpLm34fSSfTUDbRiDCi6N1nOgTOlds +-----END PRIVATE KEY----- diff --git a/cmd/osiris/internal/config/tls.go b/cmd/osiris/internal/config/tls.go new file mode 100644 index 00000000..e9f90ddd --- /dev/null +++ b/cmd/osiris/internal/config/tls.go @@ -0,0 +1,40 @@ +package config + +import ( + "crypto/tls" + "errors" + "fmt" + "os" +) + +var ( + ErrCantReadTLS = errors.New("tls: can't read TLS") + ErrInvalidTLSKeypair = errors.New("tls: can't parse TLS keypair") +) + +type TLS struct { + Cert string `hcl:"cert"` + Key string `hcl:"key"` +} + +func (t TLS) Valid() error { + var errs []error + + if _, err := os.Stat(t.Cert); err != nil { + errs = append(errs, fmt.Errorf("%w certificate %s: %w", ErrCantReadTLS, t.Cert, err)) + } + + if _, err := os.Stat(t.Key); err != nil { + errs = append(errs, fmt.Errorf("%w key %s: %w", ErrCantReadTLS, t.Key, err)) + } + + if _, err := tls.LoadX509KeyPair(t.Cert, t.Key); err != nil { + errs = append(errs, fmt.Errorf("%w (%s, %s): %w", ErrInvalidTLSKeypair, t.Cert, t.Key, err)) + } + + if len(errs) != 0 { + return errors.Join(errs...) + } + + return nil +} diff --git a/cmd/osiris/internal/config/tls_test.go b/cmd/osiris/internal/config/tls_test.go new file mode 100644 index 00000000..c4a7c6b6 --- /dev/null +++ b/cmd/osiris/internal/config/tls_test.go @@ -0,0 +1,48 @@ +package config + +import ( + "errors" + "testing" +) + +func TestTLSValid(t *testing.T) { + for _, tt := range []struct { + name string + input TLS + err error + }{ + { + name: "simple selfsigned", + input: TLS{ + Cert: "./testdata/tls/selfsigned.crt", + Key: "./testdata/tls/selfsigned.key", + }, + }, + { + name: "files don't exist", + input: TLS{ + Cert: "./testdata/tls/nonexistent.crt", + Key: "./testdata/tls/nonexistent.key", + }, + err: ErrCantReadTLS, + }, + { + name: "invalid keypair", + input: TLS{ + Cert: "./testdata/tls/invalid.crt", + Key: "./testdata/tls/invalid.key", + }, + err: ErrInvalidTLSKeypair, + }, + } { + t.Run(tt.name, func(t *testing.T) { + if err := tt.input.Valid(); !errors.Is(err, tt.err) { + t.Logf("want: %v", tt.err) + t.Logf("got: %v", err) + t.Error("got wrong error from validation function") + } else { + t.Log(err) + } + }) + } +} diff --git a/cmd/osiris/internal/entrypoint/entrypoint.go b/cmd/osiris/internal/entrypoint/entrypoint.go new file mode 100644 index 00000000..fdc643e1 --- /dev/null +++ b/cmd/osiris/internal/entrypoint/entrypoint.go @@ -0,0 +1,37 @@ +package entrypoint + +import ( + "fmt" + "log/slog" + "net/http" + + "github.com/TecharoHQ/anubis/cmd/osiris/internal/config" + "github.com/TecharoHQ/anubis/internal" + "github.com/hashicorp/hcl/v2/hclsimple" + healthv1 "google.golang.org/grpc/health/grpc_health_v1" +) + +type Options struct { + ConfigFname string +} + +func Main(opts Options) error { + internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING) + + var cfg config.Toplevel + if err := hclsimple.DecodeFile(opts.ConfigFname, nil, &cfg); err != nil { + return fmt.Errorf("can't read configuration file %s:\n\n%w", opts.ConfigFname, err) + } + + if err := cfg.Valid(); err != nil { + return fmt.Errorf("configuration file %s is invalid:\n\n%w", opts.ConfigFname, err) + } + + rtr, err := NewRouter(cfg) + if err != nil { + return err + } + + slog.Info("listening on", "http", cfg.Bind.HTTP) + return http.ListenAndServe(cfg.Bind.HTTP, rtr) +} diff --git a/cmd/osiris/internal/entrypoint/h2c.go b/cmd/osiris/internal/entrypoint/h2c.go new file mode 100644 index 00000000..68c87512 --- /dev/null +++ b/cmd/osiris/internal/entrypoint/h2c.go @@ -0,0 +1,33 @@ +package entrypoint + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/httputil" + "net/url" + + "golang.org/x/net/http2" +) + +func newH2CReverseProxy(target *url.URL) *httputil.ReverseProxy { + director := func(req *http.Request) { + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.Host = target.Host + } + + // Use h2c transport + transport := &http2.Transport{ + AllowHTTP: true, + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + // Just do plain TCP (h2c) + return net.Dial(network, addr) + }, + } + + return &httputil.ReverseProxy{ + Director: director, + Transport: transport, + } +} diff --git a/cmd/osiris/internal/entrypoint/router.go b/cmd/osiris/internal/entrypoint/router.go new file mode 100644 index 00000000..4f16f34a --- /dev/null +++ b/cmd/osiris/internal/entrypoint/router.go @@ -0,0 +1,114 @@ +package entrypoint + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "sync" + + "github.com/TecharoHQ/anubis/cmd/osiris/internal/config" + "github.com/lum8rjack/go-ja4h" +) + +var ( + ErrTargetInvalid = errors.New("[unexpected] target invalid") + ErrNoHandler = errors.New("[unexpected] no handler for domain") +) + +type Router struct { + lock sync.RWMutex + routes map[string]http.Handler +} + +func (rtr *Router) setConfig(c config.Toplevel) error { + var errs []error + newMap := map[string]http.Handler{} + + for _, d := range c.Domains { + var domainErrs []error + + u, err := url.Parse(d.Target) + if err != nil { + domainErrs = append(domainErrs, fmt.Errorf("%w %q: %v", ErrTargetInvalid, d.Target, err)) + } + + var h http.Handler + + switch u.Scheme { + case "http", "https": + h = httputil.NewSingleHostReverseProxy(u) + case "h2c": + h = newH2CReverseProxy(u) + case "unix": + h = &httputil.ReverseProxy{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", strings.TrimPrefix(d.Target, "unix://")) + }, + }, + } + } + + if h == nil { + domainErrs = append(domainErrs, ErrNoHandler) + } + + newMap[d.Name] = h + + if len(domainErrs) != 0 { + errs = append(errs, fmt.Errorf("invalid domain %s: %w", d.Name, errors.Join(domainErrs...))) + } + } + + if len(errs) != 0 { + return fmt.Errorf("can't compile config to routing map: %w", errors.Join(errs...)) + } + + rtr.lock.Lock() + rtr.routes = newMap + rtr.lock.Unlock() + + return nil +} + +func NewRouter(c config.Toplevel) (*Router, error) { + result := &Router{ + routes: map[string]http.Handler{}, + } + + if err := result.setConfig(c); err != nil { + return nil, err + } + + fmt.Printf("%#v\n", result.routes) + + return result, nil +} + +func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var h http.Handler + var ok bool + + ja4hFP := ja4h.JA4H(r) + + slog.Info("got request", "method", r.Method, "host", r.Host, "path", r.URL.Path) + + rtr.lock.RLock() + h, ok = rtr.routes[r.Host] + rtr.lock.RUnlock() + + if !ok { + http.NotFound(w, r) // TODO(Xe): brand this + return + } + + r.Header.Set("X-HTTP-JA4H-Fingerprint", ja4hFP) + + h.ServeHTTP(w, r) +} diff --git a/cmd/osiris/main.go b/cmd/osiris/main.go new file mode 100644 index 00000000..ed891438 --- /dev/null +++ b/cmd/osiris/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/TecharoHQ/anubis" + "github.com/TecharoHQ/anubis/cmd/osiris/internal/entrypoint" + "github.com/TecharoHQ/anubis/internal" + "github.com/facebookgo/flagenv" +) + +var ( + configFname = flag.String("config", "./osiris.hcl", "Configuration file (HCL), see docs") + slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)") + versionFlag = flag.Bool("version", false, "if true, show version information then quit") +) + +func main() { + flagenv.Parse() + flag.Parse() + + if *versionFlag { + fmt.Println("Osiris", anubis.Version) + return + } + + internal.InitSlog(*slogLevel) + + if err := entrypoint.Main(entrypoint.Options{ + ConfigFname: *configFname, + }); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} diff --git a/cmd/osiris/osiris.hcl b/cmd/osiris/osiris.hcl new file mode 100644 index 00000000..c20dac95 --- /dev/null +++ b/cmd/osiris/osiris.hcl @@ -0,0 +1,15 @@ +bind { + http = ":3004" + https = ":3005" + metrics = ":9091" +} + +domain "anubis.techaro.lol" { + tls { + cert = "./internal/config/testdata/tls/selfsigned.crt" + key = "./internal/config/testdata/tls/selfsigned.key" + } + + target = "http://localhost:3000" + health_target = "http://localhost:9091/healthz" +} \ No newline at end of file diff --git a/go.mod b/go.mod index c5562dbb..ecd7dcdf 100644 --- a/go.mod +++ b/go.mod @@ -47,8 +47,10 @@ require ( github.com/Songmu/gitconfig v0.2.0 // indirect github.com/TecharoHQ/yeet v0.6.0 // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect + github.com/agext/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect github.com/cavaliergopher/cpio v1.0.1 // indirect @@ -91,6 +93,7 @@ require ( github.com/goccy/go-yaml v1.12.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v70 v70.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect @@ -99,6 +102,7 @@ require ( github.com/goreleaser/fileglob v1.3.0 // indirect github.com/goreleaser/nfpm/v2 v2.42.1 // indirect github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect @@ -109,6 +113,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect @@ -147,6 +152,7 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zclconf/go-cty v1.16.3 // indirect gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect diff --git a/go.sum b/go.sum index 54da3703..211a9ab6 100644 --- a/go.sum +++ b/go.sum @@ -42,12 +42,16 @@ github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ6 github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= github.com/a-h/templ v0.3.906 h1:ZUThc8Q9n04UATaCwaG60pB1AqbulLmYEAMnWV63svg= github.com/a-h/templ v0.3.906/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -221,6 +225,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1ns github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -274,6 +280,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -395,6 +403,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= +github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=