ContainerImage.Pinniped/internal/kubeclient/kubeclient.go

234 lines
9.3 KiB
Go

// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package kubeclient
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/client-go/kubernetes"
kubescheme "k8s.io/client-go/kubernetes/scheme"
restclient "k8s.io/client-go/rest"
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
aggregatorclientscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
pinnipedconciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
pinnipedconciergeclientsetscheme "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/scheme"
pinnipedsupervisorclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
pinnipedsupervisorclientsetscheme "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/scheme"
"go.pinniped.dev/internal/crypto/ptls"
)
type Client struct {
Kubernetes kubernetes.Interface
Aggregation aggregatorclient.Interface
PinnipedConcierge pinnipedconciergeclientset.Interface
PinnipedSupervisor pinnipedsupervisorclientset.Interface
JSONConfig, ProtoConfig *restclient.Config
}
func New(opts ...Option) (*Client, error) {
c := &clientConfig{}
for _, opt := range opts {
opt(c)
}
// default to assuming we are running in a pod with the service account token mounted
if c.config == nil {
inClusterConfig, err := restclient.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
}
WithConfig(inClusterConfig)(c) // make sure all writes to clientConfig flow through one code path
}
secureKubeConfig, err := createSecureKubeConfig(c.config)
if err != nil {
return nil, fmt.Errorf("could not create secure client config: %w", err)
}
// explicitly use json when talking to CRD APIs
jsonKubeConfig := createJSONKubeConfig(secureKubeConfig)
// explicitly use protobuf when talking to built-in kube APIs
protoKubeConfig := createProtoKubeConfig(secureKubeConfig)
// Connect to the core Kubernetes API.
k8sClient, err := kubernetes.NewForConfig(configWithWrapper(protoKubeConfig, kubescheme.Scheme, kubescheme.Codecs, c.middlewares, c.transportWrapper))
if err != nil {
return nil, fmt.Errorf("could not initialize Kubernetes client: %w", err)
}
// Connect to the Kubernetes aggregation API.
aggregatorClient, err := aggregatorclient.NewForConfig(configWithWrapper(protoKubeConfig, aggregatorclientscheme.Scheme, aggregatorclientscheme.Codecs, c.middlewares, c.transportWrapper))
if err != nil {
return nil, fmt.Errorf("could not initialize aggregation client: %w", err)
}
// Connect to the pinniped concierge API.
// We cannot use protobuf encoding here because we are using CRDs
// (for which protobuf encoding is not yet supported).
pinnipedConciergeClient, err := pinnipedconciergeclientset.NewForConfig(configWithWrapper(jsonKubeConfig, pinnipedconciergeclientsetscheme.Scheme, pinnipedconciergeclientsetscheme.Codecs, c.middlewares, c.transportWrapper))
if err != nil {
return nil, fmt.Errorf("could not initialize pinniped client: %w", err)
}
// Connect to the pinniped supervisor API.
// We cannot use protobuf encoding here because we are using CRDs
// (for which protobuf encoding is not yet supported).
pinnipedSupervisorClient, err := pinnipedsupervisorclientset.NewForConfig(configWithWrapper(jsonKubeConfig, pinnipedsupervisorclientsetscheme.Scheme, pinnipedsupervisorclientsetscheme.Codecs, c.middlewares, c.transportWrapper))
if err != nil {
return nil, fmt.Errorf("could not initialize pinniped client: %w", err)
}
return &Client{
Kubernetes: k8sClient,
Aggregation: aggregatorClient,
PinnipedConcierge: pinnipedConciergeClient,
PinnipedSupervisor: pinnipedSupervisorClient,
JSONConfig: jsonKubeConfig,
ProtoConfig: protoKubeConfig,
}, nil
}
// Returns a copy of the input config with the ContentConfig set to use json.
// Use this config to communicate with all CRD based APIs.
func createJSONKubeConfig(kubeConfig *restclient.Config) *restclient.Config {
jsonKubeConfig := restclient.CopyConfig(kubeConfig)
jsonKubeConfig.AcceptContentTypes = runtime.ContentTypeJSON
jsonKubeConfig.ContentType = runtime.ContentTypeJSON
return jsonKubeConfig
}
// Returns a copy of the input config with the ContentConfig set to use protobuf.
// Do not use this config to communicate with any CRD based APIs.
func createProtoKubeConfig(kubeConfig *restclient.Config) *restclient.Config {
protoKubeConfig := restclient.CopyConfig(kubeConfig)
const protoThenJSON = runtime.ContentTypeProtobuf + "," + runtime.ContentTypeJSON
protoKubeConfig.AcceptContentTypes = protoThenJSON
protoKubeConfig.ContentType = runtime.ContentTypeProtobuf
return protoKubeConfig
}
// createSecureKubeConfig returns a copy of the input config with the WrapTransport
// enhanced to use the secure TLS configuration of the ptls / phttp packages.
func createSecureKubeConfig(kubeConfig *restclient.Config) (*restclient.Config, error) {
secureKubeConfig := restclient.CopyConfig(kubeConfig)
// by setting proxy to always be non-nil, we bust the client-go global TLS config cache.
// this is required to make our wrapper function work without data races. the unit tests
// associated with this code run in parallel to assert that we are not using the cache.
// see k8s.io/client-go/transport.tlsConfigKey
if secureKubeConfig.Proxy == nil {
secureKubeConfig.Proxy = net.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
}
// make sure restclient.TLSConfigFor always returns a non-nil TLS config
if len(secureKubeConfig.NextProtos) == 0 {
secureKubeConfig.NextProtos = ptls.Secure(nil).NextProtos
}
tlsConfigTest, err := restclient.TLSConfigFor(secureKubeConfig)
if err != nil {
return nil, err // should never happen because our input config should always be valid
}
if tlsConfigTest == nil {
return nil, fmt.Errorf("unexpected empty TLS config") // should never happen because we set NextProtos above
}
secureKubeConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper {
defer func() {
if err := AssertSecureTransport(rt); err != nil {
panic(err) // not sure what the point of this function would be if it failed to make the config secure
}
}()
tlsConfig, err := net.TLSClientConfig(rt)
if err != nil {
// this assumes none of our production code calls Wrap or messes with WrapTransport.
// this is a reasonable assumption because all such code should live in this package
// and all such code should run after this function is called, not before. the kube
// codebase uses transport wrappers that can be unwrapped to access the underlying
// TLS config.
panic(err)
}
if tlsConfig == nil {
panic("unexpected empty TLS config") // we validate this case above via tlsConfigTest
}
// mutate the TLS config into our desired state before it is used
ptls.Merge(ptls.Secure, tlsConfig)
return rt // return the input transport since we mutated it in-place
})
if err := AssertSecureConfig(secureKubeConfig); err != nil {
return nil, err // not sure what the point of this function would be if it failed to make the config secure
}
return secureKubeConfig, nil
}
// SecureAnonymousClientConfig has the same properties as restclient.AnonymousClientConfig
// while still enforcing the secure TLS configuration of the ptls / phttp packages.
func SecureAnonymousClientConfig(kubeConfig *restclient.Config) *restclient.Config {
kubeConfig = restclient.AnonymousClientConfig(kubeConfig)
secureKubeConfig, err := createSecureKubeConfig(kubeConfig)
if err != nil {
panic(err) // should never happen as this would only fail on invalid CA data, which would never work anyway
}
if err := AssertSecureConfig(secureKubeConfig); err != nil {
panic(err) // not sure what the point of this function would be if it failed to make the config secure
}
return secureKubeConfig
}
func AssertSecureConfig(kubeConfig *restclient.Config) error {
rt, err := restclient.TransportFor(kubeConfig)
if err != nil {
return fmt.Errorf("failed to build transport: %w", err)
}
return AssertSecureTransport(rt)
}
func AssertSecureTransport(rt http.RoundTripper) error {
tlsConfig, err := net.TLSClientConfig(rt)
if err != nil {
return fmt.Errorf("failed to get TLS config: %w", err)
}
tlsConfigCopy := tlsConfig.Clone()
ptls.Merge(ptls.Secure, tlsConfigCopy) // only mutate the copy
//nolint:gosec // the empty TLS config here is not used
if diff := cmp.Diff(tlsConfigCopy, tlsConfig,
cmpopts.IgnoreUnexported(tls.Config{}, x509.CertPool{}),
cmpopts.IgnoreFields(tls.Config{}, "GetClientCertificate"),
); len(diff) != 0 {
return fmt.Errorf("tls config is not secure:\n%s", diff)
}
return nil
}
func Secure(config *restclient.Config) (kubernetes.Interface, *restclient.Config, error) {
// our middleware does not apply to the returned restclient.Config, therefore, this
// client not having a leader election lock is irrelevant since it would not be enforced
secureClient, err := New(WithConfig(config)) // handles nil config correctly
if err != nil {
return nil, nil, fmt.Errorf("failed to build secure client: %w", err)
}
return secureClient.Kubernetes, secureClient.ProtoConfig, nil
}