From e85d02c53009e5f99794f785e8e365c0e663a9f2 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 12 Dec 2024 14:12:04 +0100 Subject: [PATCH] Add support dump API endpoint --- docs/content/operations/api.md | 59 +++++------ pkg/api/handler.go | 2 + pkg/api/handler_support_dump.go | 96 ++++++++++++++++++ pkg/api/handler_support_dump_test.go | 144 +++++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 29 deletions(-) create mode 100644 pkg/api/handler_support_dump.go create mode 100644 pkg/api/handler_support_dump_test.go diff --git a/docs/content/operations/api.md b/docs/content/operations/api.md index 2829f3ffe..2099c2060 100644 --- a/docs/content/operations/api.md +++ b/docs/content/operations/api.md @@ -145,34 +145,35 @@ All the following endpoints must be accessed with a `GET` HTTP request. curl https://traefik.example.com:8080/api/http/routers?page=2&per_page=20 ``` -| Path | Description | -|--------------------------------|---------------------------------------------------------------------------------------------| -| `/api/http/routers` | Lists all the HTTP routers information. | -| `/api/http/routers/{name}` | Returns the information of the HTTP router specified by `name`. | -| `/api/http/services` | Lists all the HTTP services information. | -| `/api/http/services/{name}` | Returns the information of the HTTP service specified by `name`. | -| `/api/http/middlewares` | Lists all the HTTP middlewares information. | -| `/api/http/middlewares/{name}` | Returns the information of the HTTP middleware specified by `name`. | -| `/api/tcp/routers` | Lists all the TCP routers information. | -| `/api/tcp/routers/{name}` | Returns the information of the TCP router specified by `name`. | -| `/api/tcp/services` | Lists all the TCP services information. | -| `/api/tcp/services/{name}` | Returns the information of the TCP service specified by `name`. | -| `/api/tcp/middlewares` | Lists all the TCP middlewares information. | -| `/api/tcp/middlewares/{name}` | Returns the information of the TCP middleware specified by `name`. | -| `/api/udp/routers` | Lists all the UDP routers information. | -| `/api/udp/routers/{name}` | Returns the information of the UDP router specified by `name`. | -| `/api/udp/services` | Lists all the UDP services information. | -| `/api/udp/services/{name}` | Returns the information of the UDP service specified by `name`. | -| `/api/entrypoints` | Lists all the entry points information. | -| `/api/entrypoints/{name}` | Returns the information of the entry point specified by `name`. | -| `/api/overview` | Returns statistic information about http and tcp as well as enabled features and providers. | -| `/api/rawdata` | Returns information about dynamic configurations, errors, status and dependency relations. | -| `/api/version` | Returns information about Traefik version. | -| `/debug/vars` | See the [expvar](https://golang.org/pkg/expvar/) Go documentation. | -| `/debug/pprof/` | See the [pprof Index](https://golang.org/pkg/net/http/pprof/#Index) Go documentation. | -| `/debug/pprof/cmdline` | See the [pprof Cmdline](https://golang.org/pkg/net/http/pprof/#Cmdline) Go documentation. | -| `/debug/pprof/profile` | See the [pprof Profile](https://golang.org/pkg/net/http/pprof/#Profile) Go documentation. | -| `/debug/pprof/symbol` | See the [pprof Symbol](https://golang.org/pkg/net/http/pprof/#Symbol) Go documentation. | -| `/debug/pprof/trace` | See the [pprof Trace](https://golang.org/pkg/net/http/pprof/#Trace) Go documentation. | +| Path | Description | +|--------------------------------|-----------------------------------------------------------------------------------------------------| +| `/api/http/routers` | Lists all the HTTP routers information. | +| `/api/http/routers/{name}` | Returns the information of the HTTP router specified by `name`. | +| `/api/http/services` | Lists all the HTTP services information. | +| `/api/http/services/{name}` | Returns the information of the HTTP service specified by `name`. | +| `/api/http/middlewares` | Lists all the HTTP middlewares information. | +| `/api/http/middlewares/{name}` | Returns the information of the HTTP middleware specified by `name`. | +| `/api/tcp/routers` | Lists all the TCP routers information. | +| `/api/tcp/routers/{name}` | Returns the information of the TCP router specified by `name`. | +| `/api/tcp/services` | Lists all the TCP services information. | +| `/api/tcp/services/{name}` | Returns the information of the TCP service specified by `name`. | +| `/api/tcp/middlewares` | Lists all the TCP middlewares information. | +| `/api/tcp/middlewares/{name}` | Returns the information of the TCP middleware specified by `name`. | +| `/api/udp/routers` | Lists all the UDP routers information. | +| `/api/udp/routers/{name}` | Returns the information of the UDP router specified by `name`. | +| `/api/udp/services` | Lists all the UDP services information. | +| `/api/udp/services/{name}` | Returns the information of the UDP service specified by `name`. | +| `/api/entrypoints` | Lists all the entry points information. | +| `/api/entrypoints/{name}` | Returns the information of the entry point specified by `name`. | +| `/api/overview` | Returns statistic information about http and tcp as well as enabled features and providers. | +| `/api/support-dump` | Returns an archive that contains the anonymized static configuration and the runtime configuration. | +| `/api/rawdata` | Returns information about dynamic configurations, errors, status and dependency relations. | +| `/api/version` | Returns information about Traefik version. | +| `/debug/vars` | See the [expvar](https://golang.org/pkg/expvar/) Go documentation. | +| `/debug/pprof/` | See the [pprof Index](https://golang.org/pkg/net/http/pprof/#Index) Go documentation. | +| `/debug/pprof/cmdline` | See the [pprof Cmdline](https://golang.org/pkg/net/http/pprof/#Cmdline) Go documentation. | +| `/debug/pprof/profile` | See the [pprof Profile](https://golang.org/pkg/net/http/pprof/#Profile) Go documentation. | +| `/debug/pprof/symbol` | See the [pprof Symbol](https://golang.org/pkg/net/http/pprof/#Symbol) Go documentation. | +| `/debug/pprof/trace` | See the [pprof Trace](https://golang.org/pkg/net/http/pprof/#Trace) Go documentation. | {!traefik-for-business-applications.md!} diff --git a/pkg/api/handler.go b/pkg/api/handler.go index d0f90150e..922779e61 100644 --- a/pkg/api/handler.go +++ b/pkg/api/handler.go @@ -89,6 +89,8 @@ func (h Handler) createRouter() *mux.Router { // Experimental endpoint apiRouter.Methods(http.MethodGet).Path("/api/overview").HandlerFunc(h.getOverview) + apiRouter.Methods(http.MethodGet).Path("/api/support-dump").HandlerFunc(h.getSupportDump) + apiRouter.Methods(http.MethodGet).Path("/api/entrypoints").HandlerFunc(h.getEntryPoints) apiRouter.Methods(http.MethodGet).Path("/api/entrypoints/{entryPointID}").HandlerFunc(h.getEntryPoint) diff --git a/pkg/api/handler_support_dump.go b/pkg/api/handler_support_dump.go new file mode 100644 index 000000000..08f0e0e0a --- /dev/null +++ b/pkg/api/handler_support_dump.go @@ -0,0 +1,96 @@ +package api + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/rs/zerolog/log" + "github.com/traefik/traefik/v3/pkg/redactor" + "github.com/traefik/traefik/v3/pkg/version" +) + +func (h Handler) getSupportDump(rw http.ResponseWriter, req *http.Request) { + logger := log.Ctx(req.Context()) + + staticConfig, err := redactor.Anonymize(h.staticConfig) + if err != nil { + logger.Error().Err(err).Msg("Unable to anonymize and marshal static configuration") + writeError(rw, err.Error(), http.StatusInternalServerError) + return + } + + runtimeConfig, err := json.Marshal(h.runtimeConfiguration) + if err != nil { + logger.Error().Err(err).Msg("Unable to marshal runtime configuration") + writeError(rw, err.Error(), http.StatusInternalServerError) + return + } + + tVersion, err := json.Marshal(struct { + Version string `json:"version"` + Codename string `json:"codename"` + StartDate time.Time `json:"startDate"` + }{ + Version: version.Version, + Codename: version.Codename, + StartDate: version.StartDate, + }) + if err != nil { + logger.Error().Err(err).Msg("Unable to marshal version") + writeError(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Header().Set("Content-Type", "application/gzip") + rw.Header().Set("Content-Disposition", "attachment; filename=support-dump.tar.gz") + + // Create gzip writer. + gw := gzip.NewWriter(rw) + defer gw.Close() + + // Create tar writer. + tw := tar.NewWriter(gw) + defer tw.Close() + + // Add configuration files to the archive. + if err := addFile(tw, "version.json", tVersion); err != nil { + logger.Error().Err(err).Msg("Unable to archive version file") + writeError(rw, err.Error(), http.StatusInternalServerError) + return + } + + if err := addFile(tw, "static-config.json", []byte(staticConfig)); err != nil { + logger.Error().Err(err).Msg("Unable to archive static configuration") + writeError(rw, err.Error(), http.StatusInternalServerError) + return + } + + if err := addFile(tw, "runtime-config.json", runtimeConfig); err != nil { + logger.Error().Err(err).Msg("Unable to archive runtime configuration") + writeError(rw, err.Error(), http.StatusInternalServerError) + return + } +} + +func addFile(tw *tar.Writer, name string, content []byte) error { + header := &tar.Header{ + Name: name, + Mode: 0o600, + Size: int64(len(content)), + ModTime: time.Now(), + } + + if err := tw.WriteHeader(header); err != nil { + return fmt.Errorf("writing tar header: %w", err) + } + + if _, err := tw.Write(content); err != nil { + return fmt.Errorf("writing tar content: %w", err) + } + + return nil +} diff --git a/pkg/api/handler_support_dump_test.go b/pkg/api/handler_support_dump_test.go new file mode 100644 index 000000000..f1c4d507a --- /dev/null +++ b/pkg/api/handler_support_dump_test.go @@ -0,0 +1,144 @@ +package api + +import ( + "archive/tar" + "compress/gzip" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/config/runtime" + "github.com/traefik/traefik/v3/pkg/config/static" +) + +func TestHandler_SupportDump(t *testing.T) { + testCases := []struct { + desc string + path string + confStatic static.Configuration + confDyn runtime.Configuration + validate func(t *testing.T, files map[string][]byte) + }{ + { + desc: "empty configurations", + path: "/api/support-dump", + confStatic: static.Configuration{API: &static.API{}, Global: &static.Global{}}, + confDyn: runtime.Configuration{}, + validate: func(t *testing.T, files map[string][]byte) { + t.Helper() + + require.Contains(t, files, "static-config.json") + require.Contains(t, files, "runtime-config.json") + require.Contains(t, files, "version.json") + + // Verify version.json contains version information + assert.Contains(t, string(files["version.json"]), `"version":"dev"`) + + assert.JSONEq(t, `{"global":{},"api":{}}`, string(files["static-config.json"])) + assert.Equal(t, `{}`, string(files["runtime-config.json"])) + }, + }, + { + desc: "with configuration data", + path: "/api/support-dump", + confStatic: static.Configuration{ + API: &static.API{}, + Global: &static.Global{}, + EntryPoints: map[string]*static.EntryPoint{ + "web": {Address: ":80"}, + }, + }, + confDyn: runtime.Configuration{ + Services: map[string]*runtime.ServiceInfo{ + "test-service": { + Service: &dynamic.Service{ + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{{URL: "http://127.0.0.1:8080"}}, + }, + }, + Status: runtime.StatusEnabled, + }, + }, + }, + validate: func(t *testing.T, files map[string][]byte) { + t.Helper() + + require.Contains(t, files, "static-config.json") + require.Contains(t, files, "runtime-config.json") + require.Contains(t, files, "version.json") + + // Verify version.json contains version information + assert.Contains(t, string(files["version.json"]), `"version":"dev"`) + + // Verify static config contains entry points + assert.Contains(t, string(files["static-config.json"]), `"entryPoints":{"web":{"address":"xxxx","http":{}}}`) + + // Verify runtime config contains services + assert.Contains(t, string(files["runtime-config.json"]), `"services":`) + assert.Contains(t, string(files["runtime-config.json"]), `"test-service"`) + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + handler := New(test.confStatic, &test.confDyn) + server := httptest.NewServer(handler.createRouter()) + + resp, err := http.DefaultClient.Get(server.URL + test.path) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "application/gzip", resp.Header.Get("Content-Type")) + assert.Equal(t, `attachment; filename=support-dump.tar.gz`, resp.Header.Get("Content-Disposition")) + + // Extract and validate the tar.gz contents. + files, err := extractTarGz(resp.Body) + require.NoError(t, err) + + test.validate(t, files) + }) + } +} + +// extractTarGz reads a tar.gz archive and returns a map of filename to contents +func extractTarGz(r io.Reader) (map[string][]byte, error) { + files := make(map[string][]byte) + + gzr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + header, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + + if header.Typeflag != tar.TypeReg { + continue + } + + contents, err := io.ReadAll(tr) + if err != nil { + return nil, err + } + + files[header.Name] = contents + } + + return files, nil +}