diff --git a/internal/net/phttp/phttp.go b/internal/net/phttp/phttp.go index 2a75ddda..88a80d7e 100644 --- a/internal/net/phttp/phttp.go +++ b/internal/net/phttp/phttp.go @@ -44,5 +44,6 @@ func defaultTransport() *http.Transport { func defaultWrap(rt http.RoundTripper) http.RoundTripper { rt = safeDebugWrappers(rt, transport.DebugWrappers, func() bool { return plog.Enabled(plog.LevelTrace) }) rt = transport.NewUserAgentRoundTripper(rest.DefaultKubernetesUserAgent(), rt) + rt = warningWrapper(rt, getWarningHandler()) return rt } diff --git a/internal/net/phttp/warning.go b/internal/net/phttp/warning.go new file mode 100644 index 00000000..99359461 --- /dev/null +++ b/internal/net/phttp/warning.go @@ -0,0 +1,74 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package phttp + +import ( + "net/http" + "os" + "runtime" + + "golang.org/x/term" + "k8s.io/apimachinery/pkg/util/net" + "k8s.io/client-go/rest" + + "go.pinniped.dev/internal/httputil/roundtripper" +) + +func warningWrapper(rt http.RoundTripper, handler rest.WarningHandler) http.RoundTripper { + return roundtripper.WrapFunc(rt, func(req *http.Request) (*http.Response, error) { + resp, err := rt.RoundTrip(req) + + handleWarnings(resp, handler) + + return resp, err + }) +} + +func handleWarnings(resp *http.Response, handler rest.WarningHandler) { + if resp == nil { + return + } + + warnings, _ := net.ParseWarningHeaders(resp.Header["Warning"]) // safe to ignore errors here + for _, warning := range warnings { + handler.HandleWarningHeader(warning.Code, warning.Agent, warning.Text) // client-go throws away the date + } +} + +func getWarningHandler() rest.WarningHandler { + // the client-go rest.WarningHandlers all log warnings with non-empty message and code=299, agent is ignored + + // no deduplication or color output when running from a non-terminal such as a pod + if isTerm := term.IsTerminal(int(os.Stderr.Fd())); !isTerm { + return rest.WarningLogger{} + } + + // deduplicate and attempt color warnings when running from a terminal + return rest.NewWarningWriter(os.Stderr, rest.WarningWriterOptions{ + Deduplicate: true, + Color: allowsColorOutput(), + }) +} + +// allowsColorOutput returns true if the process environment indicates color output is supported and desired. +// Copied from k8s.io/kubectl/pkg/util/term.AllowsColorOutput. +func allowsColorOutput() bool { + // https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals + if os.Getenv("TERM") == "dumb" { + return false + } + + // https://no-color.org/ + if _, nocolor := os.LookupEnv("NO_COLOR"); nocolor { + return false + } + + // On Windows WT_SESSION is set by the modern terminal component. + // Older terminals have poor support for UTF-8, VT escape codes, etc. + if runtime.GOOS == "windows" && os.Getenv("WT_SESSION") == "" { + return false + } + + return true +} diff --git a/internal/net/phttp/warning_test.go b/internal/net/phttp/warning_test.go new file mode 100644 index 00000000..4ca8a448 --- /dev/null +++ b/internal/net/phttp/warning_test.go @@ -0,0 +1,110 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package phttp + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/httputil/roundtripper" +) + +func Test_warningWrapper(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + resp *http.Response + wantCodes []int + wantAgents []string + wantTexts []string + }{ + { + name: "nil resp", + resp: nil, + wantCodes: nil, + wantAgents: nil, + wantTexts: nil, + }, + { + name: "no warning", + resp: testResp(t, http.Header{"moon": {"white"}}), //nolint:bodyclose + wantCodes: nil, + wantAgents: nil, + wantTexts: nil, + }, + { + name: "malformed warning", + resp: testResp(t, http.Header{"Warning": {"wee"}}), //nolint:bodyclose + wantCodes: nil, + wantAgents: nil, + wantTexts: nil, + }, + { + name: "partial malformed warning", + resp: testResp(t, http.Header{"Warning": {`123 foo "bar"`, "wee"}}), //nolint:bodyclose + wantCodes: []int{123}, + wantAgents: []string{"foo"}, + wantTexts: []string{"bar"}, + }, + { + name: "partial malformed warning other order", + resp: testResp(t, http.Header{"Warning": {"bar", `852 nah "dude"`, "wee"}}), //nolint:bodyclose + wantCodes: []int{852}, + wantAgents: []string{"nah"}, + wantTexts: []string{"dude"}, + }, + { + name: "multiple warnings", + resp: testResp(t, http.Header{"Warning": {`123 foo "bar"`, `222 good "day"`}}), //nolint:bodyclose + wantCodes: []int{123, 222}, + wantAgents: []string{"foo", "good"}, + wantTexts: []string{"bar", "day"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var rtCalled bool + + staticErr := constable.Error("yay pandas") + + rtFunc := roundtripper.Func(func(r *http.Request) (*http.Response, error) { + rtCalled = true + require.Nil(t, r) + return tt.resp, staticErr + }) + + h := &testWarningHandler{} + out := warningWrapper(rtFunc, h) + + resp, err := out.RoundTrip(nil) //nolint:bodyclose + + require.Equal(t, tt.resp, resp) + require.Equal(t, staticErr, err) + require.True(t, rtCalled) + + require.Equal(t, tt.wantCodes, h.codes) + require.Equal(t, tt.wantAgents, h.agents) + require.Equal(t, tt.wantTexts, h.texts) + }) + } +} + +type testWarningHandler struct { + codes []int + agents []string + texts []string +} + +func (h *testWarningHandler) HandleWarningHeader(code int, agent, text string) { + h.codes = append(h.codes, code) + h.agents = append(h.agents, agent) + h.texts = append(h.texts, text) +} diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index ae87639c..ac096051 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -21,6 +21,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/clock" + genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/pkg/version" @@ -60,7 +61,7 @@ const ( ) func startServer(ctx context.Context, shutdown *sync.WaitGroup, l net.Listener, handler http.Handler) { - server := http.Server{Handler: handler} + server := http.Server{Handler: genericapifilters.WithWarningRecorder(handler)} shutdown.Add(1) go func() {