updated vendor

This commit is contained in:
2026-06-16 08:02:19 +02:00
parent 2f7f99d3f0
commit 77299d0c64
1283 changed files with 67302 additions and 208958 deletions
+154
View File
@@ -0,0 +1,154 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package transport
import (
"bytes"
"context"
"net/http"
"os"
"sync"
"time"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/client-go/tools/metrics"
"k8s.io/klog/v2"
"k8s.io/utils/clock"
)
var _ utilnet.RoundTripperWrapper = &atomicTransportHolder{}
// atomicTransportHolder holds a transport that can be atomically updated
// when CA files change, enabling graceful CA rotation without cache complexity
type atomicTransportHolder struct {
caFile string
currentCAData []byte // Track the actual CA data currently in use
// clock and caRefreshDuration are used to allow for testing time-based logic.
clock clock.Clock
caRefreshDuration time.Duration
// mu covers transport and transportLastChecked
mu sync.RWMutex
transport *http.Transport
transportLastChecked time.Time
}
func (h *atomicTransportHolder) RoundTrip(req *http.Request) (*http.Response, error) {
return h.getTransport(req.Context()).RoundTrip(req)
}
func (h *atomicTransportHolder) WrappedRoundTripper() http.RoundTripper {
h.mu.RLock()
defer h.mu.RUnlock()
return h.transport
}
func (h *atomicTransportHolder) getTransport(ctx context.Context) *http.Transport {
if rt := h.getTransportIfFresh(); rt != nil {
return rt
}
h.mu.Lock()
defer h.mu.Unlock()
h.tryRefreshTransportLocked(ctx)
return h.transport
}
func (h *atomicTransportHolder) getTransportIfFresh() *http.Transport {
h.mu.RLock()
defer h.mu.RUnlock()
if h.clock.Since(h.transportLastChecked) < h.caRefreshDuration {
return h.transport
}
return nil
}
func (h *atomicTransportHolder) tryRefreshTransportLocked(ctx context.Context) {
// If some other goroutine already checked/updated the CA
if h.clock.Since(h.transportLastChecked) < h.caRefreshDuration {
return
}
// only attempt CA reload once per caRefreshDuration, even if the reload fails
h.transportLastChecked = h.clock.Now()
logger := klog.FromContext(ctx).WithValues("caFile", h.caFile)
logger.V(4).Info("Checking CA file content")
// Load new CA data from file
newCAData, err := os.ReadFile(h.caFile)
// Return old transport on read error
if err != nil {
logger.Error(err, "Failed to read CA data from file")
metrics.TransportCAReloads.Increment("failure", "read_error")
return
}
if len(newCAData) == 0 {
logger.Info("CA file empty, skipping transport rotation")
metrics.TransportCAReloads.Increment("failure", "empty")
return
}
if bytes.Equal(h.currentCAData, newCAData) {
logger.V(4).Info("CA file unchanged, skipping transport rotation")
metrics.TransportCAReloads.Increment("success", "unchanged")
return
}
logger.V(4).Info("CA content changed, updating transport")
// Load new CA pool
newCAs, err := rootCertPool(newCAData)
// Return old transport on parse error
if err != nil {
logger.Error(err, "Failed to parse CA data from file")
metrics.TransportCAReloads.Increment("failure", "ca_parse_error")
return
}
newTransport := h.transport.Clone()
newTransport.TLSClientConfig.RootCAs = newCAs
oldTransport := h.transport
h.transport = newTransport
// Update our tracking of current CA data
h.currentCAData = newCAData
// Close idle connections on the old transport to encourage migration
oldTransport.CloseIdleConnections()
logger.V(4).Info("Transport updated for CA rotation")
metrics.TransportCAReloads.Increment("success", "updated")
}
// newAtomicTransportHolder creates a new holder for CA file reloading scenarios.
// The caFile must be specified.
// caData may be empty but should correspond to the contents of caFile.
// transport must have a TLS config and its root CAs should match caData.
func newAtomicTransportHolder(caFile string, caData []byte, transport *http.Transport) *atomicTransportHolder {
c := clock.RealClock{}
return &atomicTransportHolder{
caFile: caFile,
currentCAData: caData,
clock: c,
caRefreshDuration: 5 * time.Minute,
transport: transport,
transportLastChecked: c.Now(),
}
}
+132 -23
View File
@@ -21,12 +21,14 @@ import (
"fmt"
"net"
"net/http"
"runtime"
"strings"
"sync"
"time"
"weak"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait"
clientgofeaturegate "k8s.io/client-go/features"
"k8s.io/client-go/tools/metrics"
"k8s.io/klog/v2"
)
@@ -35,22 +37,26 @@ import (
// same RoundTripper will be returned for configs with identical TLS options If
// the config has no custom TLS options, http.DefaultTransport is returned.
type tlsTransportCache struct {
mu sync.Mutex
transports map[tlsCacheKey]*http.Transport
mu sync.Mutex
transports map[tlsCacheKey]weak.Pointer[trackedTransport] // GC-enabled
strongTransports map[tlsCacheKey]http.RoundTripper // GC-disabled
}
// DialerStopCh is stop channel that is passed down to dynamic cert dialer.
// It's exposed as variable for testing purposes to avoid testing for goroutine
// leakages.
var DialerStopCh = wait.NeverStop
const idleConnsPerHost = 25
var tlsCache = &tlsTransportCache{transports: make(map[tlsCacheKey]*http.Transport)}
var tlsCache = newTLSCache()
func newTLSCache() *tlsTransportCache {
return &tlsTransportCache{
transports: make(map[tlsCacheKey]weak.Pointer[trackedTransport]),
strongTransports: make(map[tlsCacheKey]http.RoundTripper),
}
}
type tlsCacheKey struct {
insecure bool
caData string
caFile string
certData string
keyData string `datapolicy:"security-key"`
certFile string
@@ -68,8 +74,8 @@ func (t tlsCacheKey) String() string {
if len(t.keyData) > 0 {
keyText = "<redacted>"
}
return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, serverName:%s, disableCompression:%t, getCert:%p, dial:%p",
t.insecure, t.caData, t.certData, keyText, t.serverName, t.disableCompression, t.getCert, t.dial)
return fmt.Sprintf("insecure:%v, caData:%#v, caFile:%s, certData:%#v, keyData:%s, serverName:%s, disableCompression:%t, getCert:%p, dial:%p",
t.insecure, t.caData, t.caFile, t.certData, keyText, t.serverName, t.disableCompression, t.getCert, t.dial)
}
func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
@@ -82,14 +88,18 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
// Ensure we only create a single transport for the given TLS options
c.mu.Lock()
defer c.mu.Unlock()
defer metrics.TransportCacheEntries.Observe(len(c.transports))
defer func() { metrics.TransportCacheEntries.Observe(c.lenLocked()) }()
// See if we already have a custom transport for this config
if t, ok := c.transports[key]; ok {
metrics.TransportCreateCalls.Increment("hit")
return t, nil
if t, ok := c.getLocked(key); ok {
if t != nil {
metrics.TransportCreateCalls.Increment("hit")
return t, nil
}
metrics.TransportCreateCalls.Increment("miss-gc")
} else {
metrics.TransportCreateCalls.Increment("miss")
}
metrics.TransportCreateCalls.Increment("miss")
} else {
metrics.TransportCreateCalls.Increment("uncacheable")
}
@@ -116,6 +126,7 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
// If we use are reloading files, we need to handle certificate rotation properly
// TODO(jackkleeman): We can also add rotation here when config.HasCertCallback() is true
var cancel context.CancelFunc
if config.TLS.ReloadTLSFiles && tlsConfig != nil && tlsConfig.GetClientCertificate != nil {
// The TLS cache is a singleton, so sharing the same name for all of its
// background activity seems okay.
@@ -123,7 +134,9 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
dynamicCertDialer := certRotatingDialer(logger, tlsConfig.GetClientCertificate, dial)
tlsConfig.GetClientCertificate = dynamicCertDialer.GetClientCertificate
dial = dynamicCertDialer.connDialer.DialContext
go dynamicCertDialer.run(DialerStopCh)
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
go dynamicCertDialer.run(ctx.Done())
}
proxy := http.ProxyFromEnvironment
@@ -131,7 +144,7 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
proxy = config.Proxy
}
transport := utilnet.SetTransportDefaults(&http.Transport{
httpTransport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: proxy,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: tlsConfig,
@@ -139,13 +152,101 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
DialContext: dial,
DisableCompression: config.DisableCompression,
})
var transport http.RoundTripper = httpTransport
if canCache {
// Cache a single transport for these options
c.transports[key] = transport
if config.TLS.ReloadCAFiles && tlsConfig != nil && tlsConfig.RootCAs != nil && len(config.TLS.CAFile) > 0 {
transport = newAtomicTransportHolder(config.TLS.CAFile, config.TLS.CAData, httpTransport)
}
return transport, nil
if !canCache && cancel == nil {
return transport, nil // uncacheable config with no cert rotation - nothing to GC
}
if !clientgofeaturegate.FeatureGates().Enabled(clientgofeaturegate.ClientsAllowTLSCacheGC) {
if canCache {
c.strongTransports[key] = transport
}
return transport, nil // cancel is intentionally discarded and the cert rotation go routine leaks
}
transportWithGC := &trackedTransport{rt: transport}
if cancel != nil {
// capture metric as local var so that cleanups do not influence other tests via globals
transportCertRotationGCCalls := metrics.TransportCertRotationGCCalls
runtime.AddCleanup(transportWithGC, func(_ struct{}) {
cancel()
transportCertRotationGCCalls.Increment()
}, struct{}{})
}
if canCache {
wp := weak.Make(transportWithGC)
c.transports[key] = wp
// capture metrics as local vars so that cleanups do not influence other tests via globals
transportCacheGCCalls := metrics.TransportCacheGCCalls
transportCacheEntries := metrics.TransportCacheEntries
runtime.AddCleanup(transportWithGC, func(key tlsCacheKey) {
c.mu.Lock()
defer c.mu.Unlock()
// make sure we only delete the weak pointer created by this specific setLocked call
if c.transports[key] != wp {
transportCacheGCCalls.Increment("skipped")
return
}
delete(c.transports, key)
transportCacheGCCalls.Increment("deleted")
transportCacheEntries.Observe(c.lenLocked())
}, key)
}
return transportWithGC, nil
}
func (c *tlsTransportCache) getLocked(key tlsCacheKey) (http.RoundTripper, bool) {
if !clientgofeaturegate.FeatureGates().Enabled(clientgofeaturegate.ClientsAllowTLSCacheGC) {
v, ok := c.strongTransports[key]
return v, ok
}
wp, ok := c.transports[key]
if !ok {
return nil, false
}
v := wp.Value()
if v == nil { // avoid typed nil
return nil, true // key exists but value has been garbage collected
}
return v, true
}
func (c *tlsTransportCache) lenLocked() int {
if !clientgofeaturegate.FeatureGates().Enabled(clientgofeaturegate.ClientsAllowTLSCacheGC) {
return len(c.strongTransports)
}
return len(c.transports)
}
// trackedTransport wraps an http.RoundTripper to serve as the weak.Pointer
// target in the TLS transport cache. Dropping all references to this object
// triggers GC cleanup of the cache entry and any cert rotation goroutine.
type trackedTransport struct {
rt http.RoundTripper
}
var _ http.RoundTripper = &trackedTransport{}
var _ utilnet.RoundTripperWrapper = &trackedTransport{}
func (v *trackedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return v.rt.RoundTrip(req)
}
func (v *trackedTransport) WrappedRoundTripper() http.RoundTripper {
return v.rt
}
// tlsConfigKey returns a unique key for tls.Config objects returned from TLSConfigFor
@@ -162,7 +263,6 @@ func tlsConfigKey(c *Config) (tlsCacheKey, bool, error) {
k := tlsCacheKey{
insecure: c.TLS.Insecure,
caData: string(c.TLS.CAData),
serverName: c.TLS.ServerName,
nextProtos: strings.Join(c.TLS.NextProtos, ","),
disableCompression: c.DisableCompression,
@@ -178,5 +278,14 @@ func tlsConfigKey(c *Config) (tlsCacheKey, bool, error) {
k.keyData = string(c.TLS.KeyData)
}
if c.TLS.ReloadCAFiles {
// When reloading CA files, include CA file path in cache key instead of CA data
// This allows the CA to be reloaded from disk on each transport creation
k.caFile = c.TLS.CAFile
} else {
// When not reloading, cache the CA data directly
k.caData = string(c.TLS.CAData)
}
return k, true, nil
}
+2 -1
View File
@@ -134,7 +134,8 @@ type TLSConfig struct {
CAFile string // Path of the PEM-encoded server trusted root certificates.
CertFile string // Path of the PEM-encoded client certificate.
KeyFile string // Path of the PEM-encoded client key.
ReloadTLSFiles bool // Set to indicate that the original config provided files, and that they should be reloaded
ReloadTLSFiles bool // Set to indicate that the original config provided files, and that they should be reloaded.
ReloadCAFiles bool // Set to indicate that CA files should be reloaded from disk.
Insecure bool // Server should be accessed without verifying the certificate. For testing only.
ServerName string // Override for the server name passed to the server for SNI and used to verify certificates.
+1 -1
View File
@@ -319,7 +319,7 @@ func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response,
token = refreshedToken.AccessToken
}
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Authorization", "Bearer "+token)
return rt.rt.RoundTrip(req)
}
+20 -5
View File
@@ -28,6 +28,7 @@ import (
"time"
utilnet "k8s.io/apimachinery/pkg/util/net"
clientgofeaturegate "k8s.io/client-go/features"
"k8s.io/klog/v2"
)
@@ -211,17 +212,26 @@ func TLSConfigFor(c *Config) (*tls.Config, error) {
// KeyData, and CAFile fields, or returns an error. If no error is returned, all three fields are
// either populated or were empty to start.
func loadTLSFiles(c *Config) error {
// Check that we are purely loading CA from file
if clientgofeaturegate.FeatureGates().Enabled(clientgofeaturegate.ClientsAllowCARotation) {
if len(c.TLS.CAFile) > 0 && len(c.TLS.CAData) == 0 {
c.TLS.ReloadCAFiles = true
}
} else if c.TLS.ReloadCAFiles {
return fmt.Errorf("ReloadCAFiles=true requires ClientsAllowCARotation to be enabled")
}
// Check that we are purely loading certs and keys from files
if len(c.TLS.CertFile) > 0 && len(c.TLS.CertData) == 0 && len(c.TLS.KeyFile) > 0 && len(c.TLS.KeyData) == 0 {
c.TLS.ReloadTLSFiles = true
}
var err error
c.TLS.CAData, err = dataFromSliceOrFile(c.TLS.CAData, c.TLS.CAFile)
if err != nil {
return err
}
// Check that we are purely loading from files
if len(c.TLS.CertFile) > 0 && len(c.TLS.CertData) == 0 && len(c.TLS.KeyFile) > 0 && len(c.TLS.KeyData) == 0 {
c.TLS.ReloadTLSFiles = true
}
c.TLS.CertData, err = dataFromSliceOrFile(c.TLS.CertData, c.TLS.CertFile)
if err != nil {
return err
@@ -254,6 +264,11 @@ func rootCertPool(caData []byte) (*x509.CertPool, error) {
// code for a look at the platform specific insanity), so we'll use the fact that RootCAs == nil gives us the system values
// It doesn't allow trusting either/or, but hopefully that won't be an issue
if len(caData) == 0 {
// When the ClientsAllowCARotation feature gate is enabled, it returns an empty but non-nil pool.
// This ensures we don't fall back to system roots when a user explicitly points CAFile to a zero-byte file.
if clientgofeaturegate.FeatureGates().Enabled(clientgofeaturegate.ClientsAllowCARotation) {
return x509.NewCertPool(), nil
}
return nil, nil
}