rebase: update k8s.io packages to v0.29.0

Signed-off-by: Niels de Vos <ndevos@ibm.com>
This commit is contained in:
Niels de Vos
2023-12-20 13:23:59 +01:00
committed by mergify[bot]
parent 328a264202
commit f080b9e0c9
367 changed files with 21340 additions and 11878 deletions

View File

@ -17,6 +17,7 @@ limitations under the License.
package httpstream
import (
"errors"
"fmt"
"io"
"net/http"
@ -95,6 +96,26 @@ type Stream interface {
Identifier() uint32
}
// UpgradeFailureError encapsulates the cause for why the streaming
// upgrade request failed. Implements error interface.
type UpgradeFailureError struct {
Cause error
}
func (u *UpgradeFailureError) Error() string {
return fmt.Sprintf("unable to upgrade streaming request: %s", u.Cause)
}
// IsUpgradeFailure returns true if the passed error is (or wrapped error contains)
// the UpgradeFailureError.
func IsUpgradeFailure(err error) bool {
if err == nil {
return false
}
var upgradeErr *UpgradeFailureError
return errors.As(err, &upgradeErr)
}
// IsUpgradeRequest returns true if the given request is a connection upgrade request
func IsUpgradeRequest(req *http.Request) bool {
for _, h := range req.Header[http.CanonicalHeaderKey(HeaderConnection)] {

View File

@ -38,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/httpstream"
utilnet "k8s.io/apimachinery/pkg/util/net"
apiproxy "k8s.io/apimachinery/pkg/util/proxy"
"k8s.io/apimachinery/third_party/forked/golang/netutil"
)
@ -68,6 +69,10 @@ type SpdyRoundTripper struct {
// pingPeriod is a period for sending Ping frames over established
// connections.
pingPeriod time.Duration
// upgradeTransport is an optional substitute for dialing if present. This field is
// mutually exclusive with the "tlsConfig", "Dialer", and "proxier".
upgradeTransport http.RoundTripper
}
var _ utilnet.TLSClientConfigHolder = &SpdyRoundTripper{}
@ -76,43 +81,61 @@ var _ utilnet.Dialer = &SpdyRoundTripper{}
// NewRoundTripper creates a new SpdyRoundTripper that will use the specified
// tlsConfig.
func NewRoundTripper(tlsConfig *tls.Config) *SpdyRoundTripper {
func NewRoundTripper(tlsConfig *tls.Config) (*SpdyRoundTripper, error) {
return NewRoundTripperWithConfig(RoundTripperConfig{
TLS: tlsConfig,
TLS: tlsConfig,
UpgradeTransport: nil,
})
}
// NewRoundTripperWithProxy creates a new SpdyRoundTripper that will use the
// specified tlsConfig and proxy func.
func NewRoundTripperWithProxy(tlsConfig *tls.Config, proxier func(*http.Request) (*url.URL, error)) *SpdyRoundTripper {
func NewRoundTripperWithProxy(tlsConfig *tls.Config, proxier func(*http.Request) (*url.URL, error)) (*SpdyRoundTripper, error) {
return NewRoundTripperWithConfig(RoundTripperConfig{
TLS: tlsConfig,
Proxier: proxier,
TLS: tlsConfig,
Proxier: proxier,
UpgradeTransport: nil,
})
}
// NewRoundTripperWithConfig creates a new SpdyRoundTripper with the specified
// configuration.
func NewRoundTripperWithConfig(cfg RoundTripperConfig) *SpdyRoundTripper {
// configuration. Returns an error if the SpdyRoundTripper is misconfigured.
func NewRoundTripperWithConfig(cfg RoundTripperConfig) (*SpdyRoundTripper, error) {
// Process UpgradeTransport, which is mutually exclusive to TLSConfig and Proxier.
if cfg.UpgradeTransport != nil {
if cfg.TLS != nil || cfg.Proxier != nil {
return nil, fmt.Errorf("SpdyRoundTripper: UpgradeTransport is mutually exclusive to TLSConfig or Proxier")
}
tlsConfig, err := utilnet.TLSClientConfig(cfg.UpgradeTransport)
if err != nil {
return nil, fmt.Errorf("SpdyRoundTripper: Unable to retrieve TLSConfig from UpgradeTransport: %v", err)
}
cfg.TLS = tlsConfig
}
if cfg.Proxier == nil {
cfg.Proxier = utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
}
return &SpdyRoundTripper{
tlsConfig: cfg.TLS,
proxier: cfg.Proxier,
pingPeriod: cfg.PingPeriod,
}
tlsConfig: cfg.TLS,
proxier: cfg.Proxier,
pingPeriod: cfg.PingPeriod,
upgradeTransport: cfg.UpgradeTransport,
}, nil
}
// RoundTripperConfig is a set of options for an SpdyRoundTripper.
type RoundTripperConfig struct {
// TLS configuration used by the round tripper.
// TLS configuration used by the round tripper if UpgradeTransport not present.
TLS *tls.Config
// Proxier is a proxy function invoked on each request. Optional.
Proxier func(*http.Request) (*url.URL, error)
// PingPeriod is a period for sending SPDY Pings on the connection.
// Optional.
PingPeriod time.Duration
// UpgradeTransport is a subtitute transport used for dialing. If set,
// this field will be used instead of "TLS" and "Proxier" for connection creation.
// Optional.
UpgradeTransport http.RoundTripper
}
// TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during
@ -123,7 +146,13 @@ func (s *SpdyRoundTripper) TLSClientConfig() *tls.Config {
// Dial implements k8s.io/apimachinery/pkg/util/net.Dialer.
func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) {
conn, err := s.dial(req)
var conn net.Conn
var err error
if s.upgradeTransport != nil {
conn, err = apiproxy.DialURL(req.Context(), req.URL, s.upgradeTransport)
} else {
conn, err = s.dial(req)
}
if err != nil {
return nil, err
}

View File

@ -21,16 +21,19 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
"golang.org/x/net/websocket"
"k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/remotecommand"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2"
)
const WebSocketProtocolHeader = "Sec-Websocket-Protocol"
// The Websocket subprotocol "channel.k8s.io" prepends each binary message with a byte indicating
// the channel number (zero indexed) the message was sent on. Messages in both directions should
// prefix their messages with this channel byte. When used for remote execution, the channel numbers
@ -77,18 +80,30 @@ const (
ReadWriteChannel
)
var (
// connectionUpgradeRegex matches any Connection header value that includes upgrade
connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)")
)
// IsWebSocketRequest returns true if the incoming request contains connection upgrade headers
// for WebSockets.
func IsWebSocketRequest(req *http.Request) bool {
if !strings.EqualFold(req.Header.Get("Upgrade"), "websocket") {
return false
}
return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection")))
return httpstream.IsUpgradeRequest(req)
}
// IsWebSocketRequestWithStreamCloseProtocol returns true if the request contains headers
// identifying that it is requesting a websocket upgrade with a remotecommand protocol
// version that supports the "CLOSE" signal; false otherwise.
func IsWebSocketRequestWithStreamCloseProtocol(req *http.Request) bool {
if !IsWebSocketRequest(req) {
return false
}
requestedProtocols := strings.TrimSpace(req.Header.Get(WebSocketProtocolHeader))
for _, requestedProtocol := range strings.Split(requestedProtocols, ",") {
if protocolSupportsStreamClose(strings.TrimSpace(requestedProtocol)) {
return true
}
}
return false
}
// IgnoreReceives reads from a WebSocket until it is closed, then returns. If timeout is set, the
@ -172,15 +187,46 @@ func (conn *Conn) SetIdleTimeout(duration time.Duration) {
conn.timeout = duration
}
// SetWriteDeadline sets a timeout on writing to the websocket connection. The
// passed "duration" identifies how far into the future the write must complete
// by before the timeout fires.
func (conn *Conn) SetWriteDeadline(duration time.Duration) {
conn.ws.SetWriteDeadline(time.Now().Add(duration)) //nolint:errcheck
}
// Open the connection and create channels for reading and writing. It returns
// the selected subprotocol, a slice of channels and an error.
func (conn *Conn) Open(w http.ResponseWriter, req *http.Request) (string, []io.ReadWriteCloser, error) {
// serveHTTPComplete is channel that is closed/selected when "websocket#ServeHTTP" finishes.
serveHTTPComplete := make(chan struct{})
// Ensure panic in spawned goroutine is propagated into the parent goroutine.
panicChan := make(chan any, 1)
go func() {
defer runtime.HandleCrash()
defer conn.Close()
// If websocket server returns, propagate panic if necessary. Otherwise,
// signal HTTPServe finished by closing "serveHTTPComplete".
defer func() {
if p := recover(); p != nil {
panicChan <- p
} else {
close(serveHTTPComplete)
}
}()
websocket.Server{Handshake: conn.handshake, Handler: conn.handle}.ServeHTTP(w, req)
}()
<-conn.ready
// In normal circumstances, "websocket.Server#ServeHTTP" calls "initialize" which closes
// "conn.ready" and then blocks until serving is complete.
select {
case <-conn.ready:
klog.V(8).Infof("websocket server initialized--serving")
case <-serveHTTPComplete:
// websocket server returned before completing initialization; cleanup and return error.
conn.closeNonThreadSafe() //nolint:errcheck
return "", nil, fmt.Errorf("websocket server finished before becoming ready")
case p := <-panicChan:
panic(p)
}
rwc := make([]io.ReadWriteCloser, len(conn.channels))
for i := range conn.channels {
rwc[i] = conn.channels[i]
@ -229,20 +275,37 @@ func (conn *Conn) resetTimeout() {
}
}
// Close is only valid after Open has been called
func (conn *Conn) Close() error {
<-conn.ready
// closeNonThreadSafe cleans up by closing streams and the websocket
// connection *without* waiting for the "ready" channel.
func (conn *Conn) closeNonThreadSafe() error {
for _, s := range conn.channels {
s.Close()
}
conn.ws.Close()
return nil
var err error
if conn.ws != nil {
err = conn.ws.Close()
}
return err
}
// Close is only valid after Open has been called
func (conn *Conn) Close() error {
<-conn.ready
return conn.closeNonThreadSafe()
}
// protocolSupportsStreamClose returns true if the passed protocol
// supports the stream close signal (currently only V5 remotecommand);
// false otherwise.
func protocolSupportsStreamClose(protocol string) bool {
return protocol == remotecommand.StreamProtocolV5Name
}
// handle implements a websocket handler.
func (conn *Conn) handle(ws *websocket.Conn) {
defer conn.Close()
conn.initialize(ws)
defer conn.Close()
supportsStreamClose := protocolSupportsStreamClose(conn.selectedProtocol)
for {
conn.resetTimeout()
@ -256,6 +319,21 @@ func (conn *Conn) handle(ws *websocket.Conn) {
if len(data) == 0 {
continue
}
if supportsStreamClose && data[0] == remotecommand.StreamClose {
if len(data) != 2 {
klog.Errorf("Single channel byte should follow stream close signal. Got %d bytes", len(data)-1)
break
} else {
channel := data[1]
if int(channel) >= len(conn.channels) {
klog.Errorf("Close is targeted for a channel %d that is not valid, possible protocol error", channel)
break
}
klog.V(4).Infof("Received half-close signal from client; close %d stream", channel)
conn.channels[channel].Close() // After first Close, other closes are noop.
}
continue
}
channel := data[0]
if conn.codec == base64Codec {
channel = channel - '0'

View File

@ -16,6 +16,54 @@ limitations under the License.
// Package wsstream contains utilities for streaming content over WebSockets.
// The Conn type allows callers to multiplex multiple read/write channels over
// a single websocket. The Reader type allows an io.Reader to be copied over
// a websocket channel as binary content.
// a single websocket.
//
// "channel.k8s.io"
//
// The Websocket RemoteCommand subprotocol "channel.k8s.io" prepends each binary message with a
// byte indicating the channel number (zero indexed) the message was sent on. Messages in both
// directions should prefix their messages with this channel byte. Used for remote execution,
// the channel numbers are by convention defined to match the POSIX file-descriptors assigned
// to STDIN, STDOUT, and STDERR (0, 1, and 2). No other conversion is performed on the raw
// subprotocol - writes are sent as they are received by the server.
//
// Example client session:
//
// CONNECT http://server.com with subprotocol "channel.k8s.io"
// WRITE []byte{0, 102, 111, 111, 10} # send "foo\n" on channel 0 (STDIN)
// READ []byte{1, 10} # receive "\n" on channel 1 (STDOUT)
// CLOSE
//
// "v2.channel.k8s.io"
//
// The second Websocket subprotocol version "v2.channel.k8s.io" is the same as version 1,
// but it is the first "versioned" subprotocol.
//
// "v3.channel.k8s.io"
//
// The third version of the Websocket RemoteCommand subprotocol adds another channel
// for terminal resizing events. This channel is prepended with the byte '3', and it
// transmits two window sizes (encoding TerminalSize struct) with integers in the range
// (0,65536].
//
// "v4.channel.k8s.io"
//
// The fourth version of the Websocket RemoteCommand subprotocol adds a channel for
// errors. This channel returns structured errors containing process exit codes. The
// error is "apierrors.StatusError{}".
//
// "v5.channel.k8s.io"
//
// The fifth version of the Websocket RemoteCommand subprotocol adds a CLOSE signal,
// which is sent as the first byte of the message. The second byte is the channel
// id. This CLOSE signal is handled by the websocket server by closing the stream,
// allowing the other streams to complete transmission if necessary, and gracefully
// shutdown the connection.
//
// Example client session:
//
// CONNECT http://server.com with subprotocol "v5.channel.k8s.io"
// WRITE []byte{0, 102, 111, 111, 10} # send "foo\n" on channel 0 (STDIN)
// WRITE []byte{255, 0} # send CLOSE signal (STDIN)
// CLOSE
package wsstream // import "k8s.io/apimachinery/pkg/util/httpstream/wsstream"