From 2f366aed2e0023b074676a0d0573ed5ea2c34376 Mon Sep 17 00:00:00 2001 From: Yury Kastov Date: Sun, 5 Oct 2025 06:13:47 +0300 Subject: [PATCH] feat(config): add unix socket HTTP config loader support (#5200) Adds support for loading configuration from HTTP endpoints served over Unix domain sockets using the http+unix:// protocol scheme. --- main/confloader/external/external.go | 59 ++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index f1ad437b..787de985 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -2,6 +2,8 @@ package external import ( "bytes" + "context" + "net" "io" "net/http" "net/url" @@ -18,6 +20,9 @@ import ( func ConfigLoader(arg string) (out io.Reader, err error) { var data []byte switch { + case strings.HasPrefix(arg, "http+unix://"): + data, err = FetchUnixSocketHTTPContent(arg) + case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): data, err = FetchHTTPContent(arg) @@ -70,6 +75,60 @@ func FetchHTTPContent(target string) ([]byte, error) { return content, nil } +// Format: http+unix:///path/to/socket.sock/api/endpoint +func FetchUnixSocketHTTPContent(target string) ([]byte, error) { + path := strings.TrimPrefix(target, "http+unix://") + + if !strings.HasPrefix(path, "/") { + return nil, errors.New("unix socket path must be absolute") + } + + var socketPath, httpPath string + + sockIdx := strings.Index(path, ".sock") + if sockIdx != -1 { + socketPath = path[:sockIdx+5] + httpPath = path[sockIdx+5:] + if httpPath == "" { + httpPath = "/" + } + } else { + return nil, errors.New("cannot determine socket path, socket file should have .sock extension") + } + + if _, err := os.Stat(socketPath); err != nil { + return nil, errors.New("socket file not found: ", socketPath).Base(err) + } + + client := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", socketPath) + }, + }, + } + defer client.CloseIdleConnections() + + resp, err := client.Get("http://localhost" + httpPath) + if err != nil { + return nil, errors.New("failed to fetch from unix socket: ", socketPath).Base(err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, errors.New("unexpected HTTP status code: ", resp.StatusCode) + } + + content, err := buf.ReadAllToBytes(resp.Body) + if err != nil { + return nil, errors.New("failed to read response").Base(err) + } + + return content, nil +} + func ExtConfigLoader(files []string, reader io.Reader) (io.Reader, error) { buf, err := ctlcmd.Run(append([]string{"convert"}, files...), reader) if err != nil {