diff --git a/internal/concierge/impersonator/impersonator.go b/internal/concierge/impersonator/impersonator.go index cabdb7b42..3e72442e1 100644 --- a/internal/concierge/impersonator/impersonator.go +++ b/internal/concierge/impersonator/impersonator.go @@ -906,9 +906,54 @@ func getTransportForProtocol(restConfig *rest.Config, protocol string) (http.Rou return nil, fmt.Errorf("could not extract TLS config: %w", err) } cfg.NextProtos = []string{protocol} + + // Overriding NextProtos above is not sufficient on its own. Starting with Go 1.27, + // http2.ConfigureTransports no longer sets NextProtos itself, and instead enables http2 by + // setting http.Transport.Protocols to allow both http/1.1 and h2. On the first round trip, + // net/http lazily recomputes NextProtos from Protocols, which would undo the override above. + // Restricting Protocols to only the desired protocol makes the override stick. + if err := restrictTransportToProtocol(rt, protocol); err != nil { + return nil, err + } + if err := kubeclient.AssertSecureTransport(rt); err != nil { return nil, err // make sure we only use a secure TLS config } return rt, nil } + +// restrictTransportToProtocol configures the underlying http.Transport to speak only the given +// protocol, which must be either "h2" or "http/1.1". +func restrictTransportToProtocol(rt http.RoundTripper, protocol string) error { + t, err := unwrapHTTPTransport(rt) + if err != nil { + return err + } + + var protocols http.Protocols + switch protocol { + case "h2": + protocols.SetHTTP2(true) + case "http/1.1": + protocols.SetHTTP1(true) + default: + return fmt.Errorf("unsupported protocol %q", protocol) + } + t.Protocols = &protocols + + return nil +} + +// unwrapHTTPTransport digs through any round tripper wrappers to find the underlying +// http.Transport. This mirrors how utilnet.TLSClientConfig unwraps round trippers. +func unwrapHTTPTransport(rt http.RoundTripper) (*http.Transport, error) { + switch t := rt.(type) { + case *http.Transport: + return t, nil + case utilnet.RoundTripperWrapper: + return unwrapHTTPTransport(t.WrappedRoundTripper()) + default: + return nil, fmt.Errorf("could not find underlying http.Transport, found type: %T", rt) + } +} diff --git a/internal/config/supervisor/config_test.go b/internal/config/supervisor/config_test.go index 5125d3d31..cf9cc2aff 100644 --- a/internal/config/supervisor/config_test.go +++ b/internal/config/supervisor/config_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2026 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisor @@ -14,6 +14,7 @@ import ( "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/testutil" ) func TestFromPath(t *testing.T) { @@ -424,7 +425,11 @@ func TestFromPath(t *testing.T) { oidc: ignoreUserInfoEndpoint: "should be a struct, but is a string" `), - wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field OIDCSpec.oidc.ignoreUserInfoEndpoint of type supervisor.IgnoreUserInfoEndpointSpec", + wantError: testutil.StringBasedOnGoVersion(testutil.GoVersionDependentString{ + GoVersion: "go1.27", + BeforeVersion: "decode yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field OIDCSpec.oidc.ignoreUserInfoEndpoint of type supervisor.IgnoreUserInfoEndpointSpec", + SinceVersion: "decode yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field .oidc.ignoreUserInfoEndpoint of type supervisor.IgnoreUserInfoEndpointSpec", + }), }, } for _, test := range tests { diff --git a/internal/plog/plog_test.go b/internal/plog/plog_test.go index da4dc96da..5e91125b8 100644 --- a/internal/plog/plog_test.go +++ b/internal/plog/plog_test.go @@ -615,8 +615,11 @@ func TestPlog(t *testing.T) { case runtimeVersionSemver.Major == 1 && runtimeVersionSemver.Minor == 21: // Format of string for Go 1.21 return "func13.TestPlog.func13.1.func2" + case runtimeVersionSemver.Major == 1 && runtimeVersionSemver.Minor >= 27: + // Format of string for Go 1.27+ + return "func13.1.1" case runtimeVersionSemver.Major == 1 && runtimeVersionSemver.Minor >= 22: - // Format of string for Go 1.22+ + // Format of string for Go 1.22 - 1.26 return "func13.TestPlog.func13.1.2" default: // Format of string for Go 1.20 and below. diff --git a/internal/testutil/goversion.go b/internal/testutil/goversion.go new file mode 100644 index 000000000..d1b056e49 --- /dev/null +++ b/internal/testutil/goversion.go @@ -0,0 +1,33 @@ +// Copyright 2026 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package testutil + +import ( + "go/version" + "runtime" +) + +// GoVersionDependentString describes a string whose value depends on which version of Go is +// running the test. GoVersion should be a Go language version, e.g. "go1.27". BeforeVersion is +// the value to use on older versions of Go, and SinceVersion is the value to use on GoVersion +// and on newer versions of Go. +type GoVersionDependentString struct { + GoVersion string + BeforeVersion string + SinceVersion string +} + +// StringBasedOnGoVersion returns the value of the given GoVersionDependentString for the version +// of Go which is running the test. +func StringBasedOnGoVersion(s GoVersionDependentString) string { + lang := version.Lang(runtime.Version()) + if lang == "" { + // Not a released version of Go, e.g. a development build, so assume that it is the newest behavior. + return s.SinceVersion + } + if version.Compare(lang, version.Lang(s.GoVersion)) >= 0 { + return s.SinceVersion + } + return s.BeforeVersion +} diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index eaf70249e..6a4592cbc 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -3664,7 +3664,11 @@ func TestMaybePerformPinnipedSupervisorIDPDiscovery(t *testing.T) { { name: "when the Supervisor returns invalid discovery information, returns an error", pinnipedDiscovery: `"not-valid-discovery-claim"`, - wantErr: `could not decode the Pinniped IDP discovery document URL in OIDC discovery from "FAKE-ISSUER": json: cannot unmarshal string into Go struct field OIDCDiscoveryResponse.discovery.supervisor.pinniped.dev/v1alpha1 of type v1alpha1.OIDCDiscoveryResponseIDPEndpoint`, + wantErr: testutil.StringBasedOnGoVersion(testutil.GoVersionDependentString{ + GoVersion: "go1.27", + BeforeVersion: `could not decode the Pinniped IDP discovery document URL in OIDC discovery from "FAKE-ISSUER": json: cannot unmarshal string into Go struct field OIDCDiscoveryResponse.discovery.supervisor.pinniped.dev/v1alpha1 of type v1alpha1.OIDCDiscoveryResponseIDPEndpoint`, + SinceVersion: `could not decode the Pinniped IDP discovery document URL in OIDC discovery from "FAKE-ISSUER": json: cannot unmarshal string into Go struct field OIDCDiscoveryResponse.discovery.supervisor.pinniped.dev~1v1alpha1 of type v1alpha1.OIDCDiscoveryResponseIDPEndpoint`, + }), }, { name: "when the Supervisor has invalid pinniped_identity_providers_endpoint, returns an error",