mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-26 21:17:23 +00:00
Merge pull request #85004 from deads2k/dynamic-agg-cert
dynamic reload cluster authentication info for aggregated API servers
This commit is contained in:
commit
02af1dd62c
@ -20,6 +20,8 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
||||||
|
"//test/utils:go_default_library",
|
||||||
"//vendor/github.com/spf13/pflag:go_default_library",
|
"//vendor/github.com/spf13/pflag:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -34,8 +34,10 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/util/cert"
|
||||||
"k8s.io/kubernetes/cmd/kube-apiserver/app"
|
"k8s.io/kubernetes/cmd/kube-apiserver/app"
|
||||||
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
||||||
|
testutil "k8s.io/kubernetes/test/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TearDownFunc is to be called to tear down a test server.
|
// TearDownFunc is to be called to tear down a test server.
|
||||||
@ -45,6 +47,9 @@ type TearDownFunc func()
|
|||||||
type TestServerInstanceOptions struct {
|
type TestServerInstanceOptions struct {
|
||||||
// DisableStorageCleanup Disable the automatic storage cleanup
|
// DisableStorageCleanup Disable the automatic storage cleanup
|
||||||
DisableStorageCleanup bool
|
DisableStorageCleanup bool
|
||||||
|
|
||||||
|
// Enable cert-auth for the kube-apiserver
|
||||||
|
EnableCertAuth bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestServer return values supplied by kube-test-ApiServer
|
// TestServer return values supplied by kube-test-ApiServer
|
||||||
@ -66,6 +71,7 @@ type Logger interface {
|
|||||||
func NewDefaultTestServerOptions() *TestServerInstanceOptions {
|
func NewDefaultTestServerOptions() *TestServerInstanceOptions {
|
||||||
return &TestServerInstanceOptions{
|
return &TestServerInstanceOptions{
|
||||||
DisableStorageCleanup: false,
|
DisableStorageCleanup: false,
|
||||||
|
EnableCertAuth: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +120,6 @@ func StartTestServer(t Logger, instanceOptions *TestServerInstanceOptions, custo
|
|||||||
for _, f := range s.Flags().FlagSets {
|
for _, f := range s.Flags().FlagSets {
|
||||||
fs.AddFlagSet(f)
|
fs.AddFlagSet(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.InsecureServing.BindPort = 0
|
s.InsecureServing.BindPort = 0
|
||||||
|
|
||||||
s.SecureServing.Listener, s.SecureServing.BindPort, err = createLocalhostListenerOnFreePort()
|
s.SecureServing.Listener, s.SecureServing.BindPort, err = createLocalhostListenerOnFreePort()
|
||||||
@ -122,6 +127,37 @@ func StartTestServer(t Logger, instanceOptions *TestServerInstanceOptions, custo
|
|||||||
return result, fmt.Errorf("failed to create listener: %v", err)
|
return result, fmt.Errorf("failed to create listener: %v", err)
|
||||||
}
|
}
|
||||||
s.SecureServing.ServerCert.CertDirectory = result.TmpDir
|
s.SecureServing.ServerCert.CertDirectory = result.TmpDir
|
||||||
|
|
||||||
|
if instanceOptions.EnableCertAuth {
|
||||||
|
// create certificates for aggregation and client-cert auth
|
||||||
|
proxySigningKey, err := testutil.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
proxySigningCert, err := cert.NewSelfSignedCACert(cert.Config{CommonName: "front-proxy-ca"}, proxySigningKey)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
proxyCACertFile := path.Join(s.SecureServing.ServerCert.CertDirectory, "proxy-ca.crt")
|
||||||
|
if err := ioutil.WriteFile(proxyCACertFile, testutil.EncodeCertPEM(proxySigningCert), 0644); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
s.Authentication.RequestHeader.ClientCAFile = proxyCACertFile
|
||||||
|
clientSigningKey, err := testutil.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
clientSigningCert, err := cert.NewSelfSignedCACert(cert.Config{CommonName: "client-ca"}, clientSigningKey)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
clientCACertFile := path.Join(s.SecureServing.ServerCert.CertDirectory, "client-ca.crt")
|
||||||
|
if err := ioutil.WriteFile(clientCACertFile, testutil.EncodeCertPEM(clientSigningCert), 0644); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
s.Authentication.ClientCert.ClientCA = clientCACertFile
|
||||||
|
}
|
||||||
|
|
||||||
s.SecureServing.ExternalAddress = s.SecureServing.Listener.Addr().(*net.TCPAddr).IP // use listener addr although it is a loopback device
|
s.SecureServing.ExternalAddress = s.SecureServing.Listener.Addr().(*net.TCPAddr).IP // use listener addr although it is a loopback device
|
||||||
|
|
||||||
_, thisFile, _, ok := runtime.Caller(0)
|
_, thisFile, _, ok := runtime.Caller(0)
|
||||||
@ -134,7 +170,9 @@ func StartTestServer(t Logger, instanceOptions *TestServerInstanceOptions, custo
|
|||||||
s.Etcd.StorageConfig = *storageConfig
|
s.Etcd.StorageConfig = *storageConfig
|
||||||
s.APIEnablement.RuntimeConfig.Set("api/all=true")
|
s.APIEnablement.RuntimeConfig.Set("api/all=true")
|
||||||
|
|
||||||
fs.Parse(customFlags)
|
if err := fs.Parse(customFlags); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
completedOptions, err := app.Complete(s)
|
completedOptions, err := app.Complete(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("failed to set default ServerRunOptions: %v", err)
|
return result, fmt.Errorf("failed to set default ServerRunOptions: %v", err)
|
||||||
@ -205,7 +243,7 @@ func StartTestServer(t Logger, instanceOptions *TestServerInstanceOptions, custo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// from here the caller must call tearDown
|
// from here the caller must call tearDown
|
||||||
result.ClientConfig = server.GenericAPIServer.LoopbackClientConfig
|
result.ClientConfig = restclient.CopyConfig(server.GenericAPIServer.LoopbackClientConfig)
|
||||||
result.ClientConfig.QPS = 1000
|
result.ClientConfig.QPS = 1000
|
||||||
result.ClientConfig.Burst = 10000
|
result.ClientConfig.Burst = 10000
|
||||||
result.ServerOpts = s
|
result.ServerOpts = s
|
||||||
|
@ -5,6 +5,7 @@ go_library(
|
|||||||
srcs = [
|
srcs = [
|
||||||
"cert_key.go",
|
"cert_key.go",
|
||||||
"client_ca.go",
|
"client_ca.go",
|
||||||
|
"configmap_cafile_content.go",
|
||||||
"dynamic_cafile_content.go",
|
"dynamic_cafile_content.go",
|
||||||
"dynamic_serving_content.go",
|
"dynamic_serving_content.go",
|
||||||
"dynamic_sni_content.go",
|
"dynamic_sni_content.go",
|
||||||
@ -19,10 +20,16 @@ go_library(
|
|||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/tools/events:go_default_library",
|
"//staging/src/k8s.io/client-go/tools/events:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/util/workqueue:go_default_library",
|
"//staging/src/k8s.io/client-go/util/workqueue:go_default_library",
|
||||||
|
@ -0,0 +1,277 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 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 dynamiccertificates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
"k8s.io/klog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigMapCAController provies a CAContentProvider that can dynamically react to configmap changes
|
||||||
|
// It also fulfills the authenticator interface to provide verifyoptions
|
||||||
|
type ConfigMapCAController struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
configmapLister corev1listers.ConfigMapLister
|
||||||
|
configmapNamespace string
|
||||||
|
configmapName string
|
||||||
|
configmapKey string
|
||||||
|
// configMapInformer is tracked so that we can start these on Run
|
||||||
|
configMapInformer cache.SharedIndexInformer
|
||||||
|
|
||||||
|
// caBundle is a caBundleAndVerifier that contains the last read, non-zero length content of the file
|
||||||
|
caBundle atomic.Value
|
||||||
|
|
||||||
|
listeners []Listener
|
||||||
|
|
||||||
|
queue workqueue.RateLimitingInterface
|
||||||
|
// preRunCaches are the caches to sync before starting the work of this control loop
|
||||||
|
preRunCaches []cache.InformerSynced
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Notifier = &ConfigMapCAController{}
|
||||||
|
var _ CAContentProvider = &ConfigMapCAController{}
|
||||||
|
var _ ControllerRunner = &ConfigMapCAController{}
|
||||||
|
|
||||||
|
// NewDynamicCAFromConfigMapController returns a CAContentProvider based on a configmap that automatically reloads content.
|
||||||
|
// It is near-realtime via an informer.
|
||||||
|
func NewDynamicCAFromConfigMapController(purpose, namespace, name, key string, kubeClient kubernetes.Interface) (*ConfigMapCAController, error) {
|
||||||
|
if len(purpose) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing purpose for ca bundle")
|
||||||
|
}
|
||||||
|
if len(namespace) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing namespace for ca bundle")
|
||||||
|
}
|
||||||
|
if len(name) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing name for ca bundle")
|
||||||
|
}
|
||||||
|
if len(key) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing key for ca bundle")
|
||||||
|
}
|
||||||
|
caContentName := fmt.Sprintf("%s::%s::%s::%s", purpose, namespace, name, key)
|
||||||
|
|
||||||
|
// we construct our own informer because we need such a small subset of the information available. Just one namespace.
|
||||||
|
uncastConfigmapInformer := corev1informers.NewFilteredConfigMapInformer(kubeClient, namespace, 12*time.Hour, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, func(listOptions *v1.ListOptions) {
|
||||||
|
listOptions.FieldSelector = fields.OneTermEqualSelector("metadata.name", name).String()
|
||||||
|
})
|
||||||
|
|
||||||
|
configmapLister := corev1listers.NewConfigMapLister(uncastConfigmapInformer.GetIndexer())
|
||||||
|
|
||||||
|
c := &ConfigMapCAController{
|
||||||
|
name: caContentName,
|
||||||
|
configmapNamespace: namespace,
|
||||||
|
configmapName: name,
|
||||||
|
configmapKey: key,
|
||||||
|
configmapLister: configmapLister,
|
||||||
|
configMapInformer: uncastConfigmapInformer,
|
||||||
|
|
||||||
|
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), fmt.Sprintf("DynamicConfigMapCABundle-%s", purpose)),
|
||||||
|
preRunCaches: []cache.InformerSynced{uncastConfigmapInformer.HasSynced},
|
||||||
|
}
|
||||||
|
if err := c.loadCABundle(); err != nil {
|
||||||
|
// don't fail, but do print out a message
|
||||||
|
klog.Warningf("unable to load initial CA bundle for: %q due to: %s", c.name, err)
|
||||||
|
}
|
||||||
|
uncastConfigmapInformer.AddEventHandler(cache.FilteringResourceEventHandler{
|
||||||
|
FilterFunc: func(obj interface{}) bool {
|
||||||
|
if cast, ok := obj.(*corev1.ConfigMap); ok {
|
||||||
|
return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
|
||||||
|
}
|
||||||
|
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
|
||||||
|
if cast, ok := tombstone.Obj.(*corev1.ConfigMap); ok {
|
||||||
|
return cast.Name == c.configmapName && cast.Namespace == c.configmapNamespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true // always return true just in case. The checks are fairly cheap
|
||||||
|
},
|
||||||
|
Handler: cache.ResourceEventHandlerFuncs{
|
||||||
|
// we have a filter, so any time we're called, we may as well queue. We only ever check one configmap
|
||||||
|
// so we don't have to be choosy about our key.
|
||||||
|
AddFunc: func(obj interface{}) {
|
||||||
|
c.queue.Add(c.keyFn())
|
||||||
|
},
|
||||||
|
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||||
|
c.queue.Add(c.keyFn())
|
||||||
|
},
|
||||||
|
DeleteFunc: func(obj interface{}) {
|
||||||
|
c.queue.Add(c.keyFn())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigMapCAController) keyFn() string {
|
||||||
|
// this format matches DeletionHandlingMetaNamespaceKeyFunc for our single key
|
||||||
|
return c.configmapNamespace + "/" + c.configmapName
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddListener adds a listener to be notified when the CA content changes.
|
||||||
|
func (c *ConfigMapCAController) AddListener(listener Listener) {
|
||||||
|
c.listeners = append(c.listeners, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadCABundle determines the next set of content for the file.
|
||||||
|
func (c *ConfigMapCAController) loadCABundle() error {
|
||||||
|
configMap, err := c.configmapLister.ConfigMaps(c.configmapNamespace).Get(c.configmapName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
caBundle := configMap.Data[c.configmapKey]
|
||||||
|
if len(caBundle) == 0 {
|
||||||
|
return fmt.Errorf("missing content for CA bundle %q", c.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// check to see if we have a change. If the values are the same, do nothing.
|
||||||
|
if !c.hasCAChanged([]byte(caBundle)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
caBundleAndVerifier, err := newCABundleAndVerifier(c.Name(), []byte(caBundle))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.caBundle.Store(caBundleAndVerifier)
|
||||||
|
|
||||||
|
for _, listener := range c.listeners {
|
||||||
|
listener.Enqueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasCAChanged returns true if the caBundle is different than the current.
|
||||||
|
func (c *ConfigMapCAController) hasCAChanged(caBundle []byte) bool {
|
||||||
|
uncastExisting := c.caBundle.Load()
|
||||||
|
if uncastExisting == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check to see if we have a change. If the values are the same, do nothing.
|
||||||
|
existing, ok := uncastExisting.(*caBundleAndVerifier)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !bytes.Equal(existing.caBundle, caBundle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunOnce runs a single sync loop
|
||||||
|
func (c *ConfigMapCAController) RunOnce() error {
|
||||||
|
// Ignore the error when running once because when using a dynamically loaded ca file, because we think it's better to have nothing for
|
||||||
|
// a brief time than completely crash. If crashing is necessary, higher order logic like a healthcheck and cause failures.
|
||||||
|
_ = c.loadCABundle()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the kube-apiserver and blocks until stopCh is closed.
|
||||||
|
func (c *ConfigMapCAController) Run(workers int, stopCh <-chan struct{}) {
|
||||||
|
defer utilruntime.HandleCrash()
|
||||||
|
defer c.queue.ShutDown()
|
||||||
|
|
||||||
|
klog.Infof("Starting %s", c.name)
|
||||||
|
defer klog.Infof("Shutting down %s", c.name)
|
||||||
|
|
||||||
|
// we have a personal informer that is narrowly scoped, start it.
|
||||||
|
go c.configMapInformer.Run(stopCh)
|
||||||
|
|
||||||
|
// wait for your secondary caches to fill before starting your work
|
||||||
|
if !cache.WaitForNamedCacheSync(c.name, stopCh, c.preRunCaches...) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't matter what workers say, only start one.
|
||||||
|
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||||
|
|
||||||
|
// start timer that rechecks every minute, just in case. this also serves to prime the controller quickly.
|
||||||
|
_ = wait.PollImmediateUntil(FileRefreshDuration, func() (bool, error) {
|
||||||
|
c.queue.Add(workItemKey)
|
||||||
|
return false, nil
|
||||||
|
}, stopCh)
|
||||||
|
|
||||||
|
<-stopCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigMapCAController) runWorker() {
|
||||||
|
for c.processNextWorkItem() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigMapCAController) processNextWorkItem() bool {
|
||||||
|
dsKey, quit := c.queue.Get()
|
||||||
|
if quit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer c.queue.Done(dsKey)
|
||||||
|
|
||||||
|
err := c.loadCABundle()
|
||||||
|
if err == nil {
|
||||||
|
c.queue.Forget(dsKey)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
|
||||||
|
c.queue.AddRateLimited(dsKey)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name is just an identifier
|
||||||
|
func (c *ConfigMapCAController) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentCABundleContent provides ca bundle byte content
|
||||||
|
func (c *ConfigMapCAController) CurrentCABundleContent() []byte {
|
||||||
|
uncastObj := c.caBundle.Load()
|
||||||
|
if uncastObj == nil {
|
||||||
|
return nil // this can happen if we've been unable load data from the apiserver for some reason
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.caBundle.Load().(*caBundleAndVerifier).caBundle
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyOptions provides verifyoptions compatible with authenticators
|
||||||
|
func (c *ConfigMapCAController) VerifyOptions() (x509.VerifyOptions, bool) {
|
||||||
|
uncastObj := c.caBundle.Load()
|
||||||
|
if uncastObj == nil {
|
||||||
|
// This can happen if we've been unable load data from the apiserver for some reason.
|
||||||
|
// In this case, we should not accept any connections on the basis of this ca bundle.
|
||||||
|
return x509.VerifyOptions{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return uncastObj.(*caBundleAndVerifier).verifyOptions, true
|
||||||
|
}
|
@ -103,10 +103,9 @@ func (c *DynamicServingCertificateController) newTLSContent() (*dynamicCertifica
|
|||||||
|
|
||||||
if c.clientCA != nil {
|
if c.clientCA != nil {
|
||||||
currClientCABundle := c.clientCA.CurrentCABundleContent()
|
currClientCABundle := c.clientCA.CurrentCABundleContent()
|
||||||
// don't remove all content. The value was configured at one time, so continue using that.
|
// we allow removing all client ca bundles because the server is still secure when this happens. it just means
|
||||||
if len(currClientCABundle) == 0 {
|
// that there isn't a hint to clients about which client-cert to used. this happens when there is no client-ca
|
||||||
return nil, fmt.Errorf("not loading an empty client ca bundle from %q", c.clientCA.Name())
|
// yet known for authentication, which can happen in aggregated apiservers and some kube-apiserver deployment modes.
|
||||||
}
|
|
||||||
newContent.clientCA = caBundleContent{caBundle: currClientCABundle}
|
newContent.clientCA = caBundleContent{caBundle: currClientCABundle}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +151,7 @@ func (c *DynamicServingCertificateController) syncCerts() error {
|
|||||||
newClientCAPool := x509.NewCertPool()
|
newClientCAPool := x509.NewCertPool()
|
||||||
newClientCAs, err := cert.ParseCertsPEM(newContent.clientCA.caBundle)
|
newClientCAs, err := cert.ParseCertsPEM(newContent.clientCA.caBundle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to load client CA file: %v", err)
|
return fmt.Errorf("unable to load client CA file %q: %v", string(newContent.clientCA.caBundle), err)
|
||||||
}
|
}
|
||||||
for i, cert := range newClientCAs {
|
for i, cert := range newClientCAs {
|
||||||
klog.V(2).Infof("loaded client CA [%d/%q]: %s", i, c.clientCA.Name(), GetHumanCertDetail(cert))
|
klog.V(2).Infof("loaded client CA [%d/%q]: %s", i, c.clientCA.Name(), GetHumanCertDetail(cert))
|
||||||
|
@ -99,12 +99,6 @@ func TestNewStaticCertKeyContent(t *testing.T) {
|
|||||||
sniCerts: []sniCertKeyContent{{certKeyContent: certKeyContent{cert: serverCert, key: serverKey}, sniNames: []string{"foo"}}},
|
sniCerts: []sniCertKeyContent{{certKeyContent: certKeyContent{cert: serverCert, key: serverKey}, sniNames: []string{"foo"}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "missingCA",
|
|
||||||
clientCA: &staticCAContent{name: "test-ca", caBundle: &caBundleAndVerifier{caBundle: []byte("")}},
|
|
||||||
expected: nil,
|
|
||||||
expectedErr: `not loading an empty client ca bundle from "test-ca"`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "nil",
|
name: "nil",
|
||||||
expected: &dynamicCertificateContent{clientCA: caBundleContent{}, servingCert: certKeyContent{}},
|
expected: &dynamicCertificateContent{clientCA: caBundleContent{}, servingCert: certKeyContent{}},
|
||||||
|
@ -48,7 +48,9 @@ func (c unionCAContent) Name() string {
|
|||||||
func (c unionCAContent) CurrentCABundleContent() []byte {
|
func (c unionCAContent) CurrentCABundleContent() []byte {
|
||||||
caBundles := [][]byte{}
|
caBundles := [][]byte{}
|
||||||
for _, curr := range c {
|
for _, curr := range c {
|
||||||
caBundles = append(caBundles, curr.CurrentCABundleContent())
|
if currCABytes := curr.CurrentCABundleContent(); len(currCABytes) > 0 {
|
||||||
|
caBundles = append(caBundles, []byte(strings.TrimSpace(string(currCABytes))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes.Join(caBundles, []byte("\n"))
|
return bytes.Join(caBundles, []byte("\n"))
|
||||||
|
@ -19,7 +19,6 @@ package options
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,7 +26,6 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
|
"k8s.io/apiserver/pkg/authentication/authenticatorfactory"
|
||||||
@ -251,38 +249,63 @@ func (s *DelegatingAuthenticationOptions) ApplyTo(authenticationInfo *server.Aut
|
|||||||
cfg.TokenAccessReviewClient = client.AuthenticationV1().TokenReviews()
|
cfg.TokenAccessReviewClient = client.AuthenticationV1().TokenReviews()
|
||||||
}
|
}
|
||||||
|
|
||||||
// look into configmaps/external-apiserver-authentication for missing authn info
|
// get the clientCA information
|
||||||
if !s.SkipInClusterLookup {
|
clientCAFileSpecified := len(s.ClientCert.ClientCA) > 0
|
||||||
err := s.lookupMissingConfigInCluster(client)
|
var clientCAProvider dynamiccertificates.CAContentProvider
|
||||||
|
if clientCAFileSpecified {
|
||||||
|
clientCAProvider, err = s.ClientCert.GetClientCAContentProvider()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load client CA file %q: %v", s.ClientCert.ClientCA, err)
|
||||||
|
}
|
||||||
|
cfg.ClientCertificateCAContentProvider = clientCAProvider
|
||||||
|
if err = authenticationInfo.ApplyClientCert(cfg.ClientCertificateCAContentProvider, servingInfo); err != nil {
|
||||||
|
return fmt.Errorf("unable to assign client CA file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if !s.SkipInClusterLookup {
|
||||||
|
if client == nil {
|
||||||
|
klog.Warningf("No authentication-kubeconfig provided in order to lookup client-ca-file in configmap/%s in %s, so client certificate authentication won't work.", authenticationConfigMapName, authenticationConfigMapNamespace)
|
||||||
|
} else {
|
||||||
|
clientCAProvider, err = dynamiccertificates.NewDynamicCAFromConfigMapController("client-ca", authenticationConfigMapNamespace, authenticationConfigMapName, "client-ca-file", client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load configmap based client CA file: %v", err)
|
||||||
|
}
|
||||||
|
cfg.ClientCertificateCAContentProvider = clientCAProvider
|
||||||
|
if err = authenticationInfo.ApplyClientCert(cfg.ClientCertificateCAContentProvider, servingInfo); err != nil {
|
||||||
|
return fmt.Errorf("unable to assign configmap based client CA file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestHeaderCAFileSpecified := len(s.RequestHeader.ClientCAFile) > 0
|
||||||
|
var requestHeaderConfig *authenticatorfactory.RequestHeaderConfig
|
||||||
|
if requestHeaderCAFileSpecified {
|
||||||
|
requestHeaderConfig, err = s.RequestHeader.ToAuthenticationRequestHeaderConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create request header authentication config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if !s.SkipInClusterLookup {
|
||||||
|
if client == nil {
|
||||||
|
klog.Warningf("No authentication-kubeconfig provided in order to lookup requestheader-client-ca-file in configmap/%s in %s, so request-header client certificate authentication won't work.", authenticationConfigMapName, authenticationConfigMapNamespace)
|
||||||
|
} else {
|
||||||
|
requestHeaderConfig, err = s.createRequestHeaderConfig(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if s.TolerateInClusterLookupFailure {
|
if s.TolerateInClusterLookupFailure {
|
||||||
klog.Warningf("Error looking up in-cluster authentication configuration: %v", err)
|
klog.Warningf("Error looking up in-cluster authentication configuration: %v", err)
|
||||||
klog.Warningf("Continuing without authentication configuration. This may treat all requests as anonymous.")
|
klog.Warningf("Continuing without authentication configuration. This may treat all requests as anonymous.")
|
||||||
klog.Warningf("To require authentication configuration lookup to succeed, set --authentication-tolerate-lookup-failure=false")
|
klog.Warningf("To require authentication configuration lookup to succeed, set --authentication-tolerate-lookup-failure=false")
|
||||||
} else {
|
} else {
|
||||||
return err
|
return fmt.Errorf("unable to load configmap based request-header-client-ca-file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// configure AuthenticationInfo config
|
|
||||||
cfg.ClientCertificateCAContentProvider, err = s.ClientCert.GetClientCAContentProvider()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to load client CA file: %v", err)
|
|
||||||
}
|
}
|
||||||
if cfg.ClientCertificateCAContentProvider != nil {
|
if requestHeaderConfig != nil {
|
||||||
if err = authenticationInfo.ApplyClientCert(cfg.ClientCertificateCAContentProvider, servingInfo); err != nil {
|
cfg.RequestHeaderConfig = requestHeaderConfig
|
||||||
return fmt.Errorf("unable to load client CA file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.RequestHeaderConfig, err = s.RequestHeader.ToAuthenticationRequestHeaderConfig()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create request header authentication config: %v", err)
|
|
||||||
}
|
|
||||||
if cfg.RequestHeaderConfig != nil {
|
|
||||||
if err = authenticationInfo.ApplyClientCert(cfg.RequestHeaderConfig.CAContentProvider, servingInfo); err != nil {
|
if err = authenticationInfo.ApplyClientCert(cfg.RequestHeaderConfig.CAContentProvider, servingInfo); err != nil {
|
||||||
return fmt.Errorf("unable to load client CA file: %v", err)
|
return fmt.Errorf("unable to load request-header-client-ca-file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,97 +333,26 @@ const (
|
|||||||
authenticationRoleName = "extension-apiserver-authentication-reader"
|
authenticationRoleName = "extension-apiserver-authentication-reader"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *DelegatingAuthenticationOptions) lookupMissingConfigInCluster(client kubernetes.Interface) error {
|
func (s *DelegatingAuthenticationOptions) createRequestHeaderConfig(client kubernetes.Interface) (*authenticatorfactory.RequestHeaderConfig, error) {
|
||||||
if len(s.ClientCert.ClientCA) > 0 && len(s.RequestHeader.ClientCAFile) > 0 {
|
requestHeaderCAProvider, err := dynamiccertificates.NewDynamicCAFromConfigMapController("client-ca", authenticationConfigMapNamespace, authenticationConfigMapName, "requestheader-client-ca-file", client)
|
||||||
return nil
|
if err != nil {
|
||||||
}
|
return nil, fmt.Errorf("unable to create request header authentication config: %v", err)
|
||||||
if client == nil {
|
|
||||||
if len(s.ClientCert.ClientCA) == 0 {
|
|
||||||
klog.Warningf("No authentication-kubeconfig provided in order to lookup client-ca-file in configmap/%s in %s, so client certificate authentication won't work.", authenticationConfigMapName, authenticationConfigMapNamespace)
|
|
||||||
}
|
|
||||||
if len(s.RequestHeader.ClientCAFile) == 0 {
|
|
||||||
klog.Warningf("No authentication-kubeconfig provided in order to lookup requestheader-client-ca-file in configmap/%s in %s, so request-header client certificate authentication won't work.", authenticationConfigMapName, authenticationConfigMapNamespace)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authConfigMap, err := client.CoreV1().ConfigMaps(authenticationConfigMapNamespace).Get(authenticationConfigMapName, metav1.GetOptions{})
|
authConfigMap, err := client.CoreV1().ConfigMaps(authenticationConfigMapNamespace).Get(authenticationConfigMapName, metav1.GetOptions{})
|
||||||
switch {
|
switch {
|
||||||
case errors.IsNotFound(err):
|
case errors.IsNotFound(err):
|
||||||
// ignore, authConfigMap is nil now
|
// ignore, authConfigMap is nil now
|
||||||
|
return nil, nil
|
||||||
case errors.IsForbidden(err):
|
case errors.IsForbidden(err):
|
||||||
klog.Warningf("Unable to get configmap/%s in %s. Usually fixed by "+
|
klog.Warningf("Unable to get configmap/%s in %s. Usually fixed by "+
|
||||||
"'kubectl create rolebinding -n %s ROLEBINDING_NAME --role=%s --serviceaccount=YOUR_NS:YOUR_SA'",
|
"'kubectl create rolebinding -n %s ROLEBINDING_NAME --role=%s --serviceaccount=YOUR_NS:YOUR_SA'",
|
||||||
authenticationConfigMapName, authenticationConfigMapNamespace, authenticationConfigMapNamespace, authenticationRoleName)
|
authenticationConfigMapName, authenticationConfigMapNamespace, authenticationConfigMapNamespace, authenticationRoleName)
|
||||||
return err
|
return nil, err
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.ClientCert.ClientCA) == 0 {
|
|
||||||
if authConfigMap != nil {
|
|
||||||
opt, err := inClusterClientCA(authConfigMap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if opt != nil {
|
|
||||||
s.ClientCert = *opt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s.ClientCert.ClientCA) == 0 {
|
|
||||||
klog.Warningf("Cluster doesn't provide client-ca-file in configmap/%s in %s, so client certificate authentication won't work.", authenticationConfigMapName, authenticationConfigMapNamespace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.RequestHeader.ClientCAFile) == 0 {
|
|
||||||
if authConfigMap != nil {
|
|
||||||
opt, err := inClusterRequestHeader(authConfigMap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if opt != nil {
|
|
||||||
s.RequestHeader = *opt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s.RequestHeader.ClientCAFile) == 0 {
|
|
||||||
klog.Warningf("Cluster doesn't provide requestheader-client-ca-file in configmap/%s in %s, so request-header client certificate authentication won't work.", authenticationConfigMapName, authenticationConfigMapNamespace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func inClusterClientCA(authConfigMap *v1.ConfigMap) (*ClientCertAuthenticationOptions, error) {
|
|
||||||
clientCA, ok := authConfigMap.Data["client-ca-file"]
|
|
||||||
if !ok {
|
|
||||||
// not having a client-ca is fine, return nil
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
clientCAProvider, err := dynamiccertificates.NewStaticCAContent("client-ca-file", []byte(clientCA))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ClientCertAuthenticationOptions{
|
|
||||||
ClientCA: "",
|
|
||||||
CAContentProvider: clientCAProvider,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func inClusterRequestHeader(authConfigMap *v1.ConfigMap) (*RequestHeaderAuthenticationOptions, error) {
|
|
||||||
requestHeaderCA, ok := authConfigMap.Data["requestheader-client-ca-file"]
|
|
||||||
if !ok {
|
|
||||||
// not having a requestheader-client-ca is fine, return nil
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := ioutil.TempFile("", "requestheader-client-ca-file")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := ioutil.WriteFile(f.Name(), []byte(requestHeaderCA), 0600); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
usernameHeaders, err := deserializeStrings(authConfigMap.Data["requestheader-username-headers"])
|
usernameHeaders, err := deserializeStrings(authConfigMap.Data["requestheader-username-headers"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -418,12 +370,12 @@ func inClusterRequestHeader(authConfigMap *v1.ConfigMap) (*RequestHeaderAuthenti
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &RequestHeaderAuthenticationOptions{
|
return &authenticatorfactory.RequestHeaderConfig{
|
||||||
UsernameHeaders: usernameHeaders,
|
CAContentProvider: requestHeaderCAProvider,
|
||||||
GroupHeaders: groupHeaders,
|
UsernameHeaders: headerrequest.StaticStringSlice(usernameHeaders),
|
||||||
ExtraHeaderPrefixes: extraHeaderPrefixes,
|
GroupHeaders: headerrequest.StaticStringSlice(groupHeaders),
|
||||||
ClientCAFile: f.Name(),
|
ExtraHeaderPrefixes: headerrequest.StaticStringSlice(extraHeaderPrefixes),
|
||||||
AllowedNames: allowedNames,
|
AllowedClientNames: headerrequest.StaticStringSlice(allowedNames),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,17 +81,19 @@ func (s *SecureServingInfo) tlsConfig(stopCh <-chan struct{}) (*tls.Config, erro
|
|||||||
}
|
}
|
||||||
// start controllers if possible
|
// start controllers if possible
|
||||||
if controller, ok := s.ClientCA.(dynamiccertificates.ControllerRunner); ok {
|
if controller, ok := s.ClientCA.(dynamiccertificates.ControllerRunner); ok {
|
||||||
// runonce to be sure that we have a value.
|
// runonce to try to prime data. If this fails, it's ok because we fail closed.
|
||||||
|
// Files are required to be populated already, so this is for convenience.
|
||||||
if err := controller.RunOnce(); err != nil {
|
if err := controller.RunOnce(); err != nil {
|
||||||
return nil, err
|
klog.Warningf("Initial population of client CA failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go controller.Run(1, stopCh)
|
go controller.Run(1, stopCh)
|
||||||
}
|
}
|
||||||
if controller, ok := s.Cert.(dynamiccertificates.ControllerRunner); ok {
|
if controller, ok := s.Cert.(dynamiccertificates.ControllerRunner); ok {
|
||||||
// runonce to be sure that we have a value.
|
// runonce to try to prime data. If this fails, it's ok because we fail closed.
|
||||||
|
// Files are required to be populated already, so this is for convenience.
|
||||||
if err := controller.RunOnce(); err != nil {
|
if err := controller.RunOnce(); err != nil {
|
||||||
return nil, err
|
klog.Warningf("Initial population of default serving certificate failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go controller.Run(1, stopCh)
|
go controller.Run(1, stopCh)
|
||||||
@ -102,18 +104,20 @@ func (s *SecureServingInfo) tlsConfig(stopCh <-chan struct{}) (*tls.Config, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if controller, ok := sniCert.(dynamiccertificates.ControllerRunner); ok {
|
if controller, ok := sniCert.(dynamiccertificates.ControllerRunner); ok {
|
||||||
// runonce to be sure that we have a value.
|
// runonce to try to prime data. If this fails, it's ok because we fail closed.
|
||||||
|
// Files are required to be populated already, so this is for convenience.
|
||||||
if err := controller.RunOnce(); err != nil {
|
if err := controller.RunOnce(); err != nil {
|
||||||
return nil, err
|
klog.Warningf("Initial population of SNI serving certificate failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go controller.Run(1, stopCh)
|
go controller.Run(1, stopCh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runonce to be sure that we have a value.
|
// runonce to try to prime data. If this fails, it's ok because we fail closed.
|
||||||
|
// Files are required to be populated already, so this is for convenience.
|
||||||
if err := dynamicCertificateController.RunOnce(); err != nil {
|
if err := dynamicCertificateController.RunOnce(); err != nil {
|
||||||
return nil, err
|
klog.Warningf("Initial population of dynamic certificates failed: %v", err)
|
||||||
}
|
}
|
||||||
go dynamicCertificateController.Run(1, stopCh)
|
go dynamicCertificateController.Run(1, stopCh)
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ go_library(
|
|||||||
"csr.go",
|
"csr.go",
|
||||||
"io.go",
|
"io.go",
|
||||||
"pem.go",
|
"pem.go",
|
||||||
|
"server_inspection.go",
|
||||||
],
|
],
|
||||||
importmap = "k8s.io/kubernetes/vendor/k8s.io/client-go/util/cert",
|
importmap = "k8s.io/kubernetes/vendor/k8s.io/client-go/util/cert",
|
||||||
importpath = "k8s.io/client-go/util/cert",
|
importpath = "k8s.io/client-go/util/cert",
|
||||||
|
97
staging/src/k8s.io/client-go/util/cert/server_inspection.go
Normal file
97
staging/src/k8s.io/client-go/util/cert/server_inspection.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2019 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 cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetClientCANames gets the CA names for client certs that a server accepts. This is useful when inspecting the
|
||||||
|
// state of particular servers. apiHost is "host:port"
|
||||||
|
func GetClientCANames(apiHost string) ([]string, error) {
|
||||||
|
// when we run this the second time, we know which one we are expecting
|
||||||
|
acceptableCAs := []string{}
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: true, // this is insecure to always get to the GetClientCertificate
|
||||||
|
GetClientCertificate: func(hello *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||||
|
acceptableCAs = []string{}
|
||||||
|
for _, curr := range hello.AcceptableCAs {
|
||||||
|
acceptableCAs = append(acceptableCAs, string(curr))
|
||||||
|
}
|
||||||
|
return &tls.Certificate{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := tls.Dial("tcp", apiHost, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
return acceptableCAs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientCANamesForURL is GetClientCANames against a URL string like we use in kubeconfigs
|
||||||
|
func GetClientCANamesForURL(kubeConfigURL string) ([]string, error) {
|
||||||
|
apiserverURL, err := url.Parse(kubeConfigURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return GetClientCANames(apiserverURL.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServingCertificates returns the x509 certs used by a server as certificates and pem encoded bytes.
|
||||||
|
// The serverName is optional for specifying a different name to get SNI certificates. apiHost is "host:port"
|
||||||
|
func GetServingCertificates(apiHost, serverName string) ([]*x509.Certificate, [][]byte, error) {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: true, // this is insecure so that we always get connected
|
||||||
|
}
|
||||||
|
// if a name is specified for SNI, set it.
|
||||||
|
if len(serverName) > 0 {
|
||||||
|
tlsConfig.ServerName = serverName
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := tls.Dial("tcp", apiHost, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
peerCerts := conn.ConnectionState().PeerCertificates
|
||||||
|
peerCertBytes := [][]byte{}
|
||||||
|
for _, a := range peerCerts {
|
||||||
|
actualCert, err := EncodeCertificates(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
peerCertBytes = append(peerCertBytes, []byte(strings.TrimSpace(string(actualCert))))
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerCerts, peerCertBytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServingCertificatesForURL is GetServingCertificates against a URL string like we use in kubeconfigs
|
||||||
|
func GetServingCertificatesForURL(kubeConfigURL, serverName string) ([]*x509.Certificate, [][]byte, error) {
|
||||||
|
apiserverURL, err := url.Parse(kubeConfigURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return GetServingCertificates(apiserverURL.Host, serverName)
|
||||||
|
}
|
@ -14,6 +14,7 @@ go_test(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/server/dynamiccertificates:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/server/dynamiccertificates:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||||
|
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
||||||
"//staging/src/k8s.io/component-base/cli/flag:go_default_library",
|
"//staging/src/k8s.io/component-base/cli/flag:go_default_library",
|
||||||
"//test/integration/framework:go_default_library",
|
"//test/integration/framework:go_default_library",
|
||||||
],
|
],
|
||||||
|
@ -18,11 +18,8 @@ package podlogs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"fmt"
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -33,6 +30,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/util/cert"
|
||||||
"k8s.io/component-base/cli/flag"
|
"k8s.io/component-base/cli/flag"
|
||||||
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
@ -88,13 +86,9 @@ MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
|
|||||||
dynamiccertificates.FileRefreshDuration = 1 * time.Second
|
dynamiccertificates.FileRefreshDuration = 1 * time.Second
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
apiserverURL, err := url.Parse(kubeconfig.Host)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for request header info
|
// wait for request header info
|
||||||
err = wait.PollImmediate(100*time.Millisecond, 30*time.Second, waitForConfigMapCAContent(t, kubeClient, "requestheader-client-ca-file", "-----BEGIN CERTIFICATE-----", 1))
|
err := wait.PollImmediate(100*time.Millisecond, 30*time.Second, waitForConfigMapCAContent(t, kubeClient, "requestheader-client-ca-file", "-----BEGIN CERTIFICATE-----", 1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -105,24 +99,6 @@ MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
|
|||||||
}
|
}
|
||||||
|
|
||||||
// when we run this the second time, we know which one we are expecting
|
// when we run this the second time, we know which one we are expecting
|
||||||
acceptableCAs := []string{}
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
GetClientCertificate: func(hello *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
|
||||||
acceptableCAs = []string{}
|
|
||||||
for _, curr := range hello.AcceptableCAs {
|
|
||||||
acceptableCAs = append(acceptableCAs, string(curr))
|
|
||||||
}
|
|
||||||
return &tls.Certificate{}, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := tls.Dial("tcp", apiserverURL.Host, tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(clientCAFilename, differentClientCA, 0644); err != nil {
|
if err := ioutil.WriteFile(clientCAFilename, differentClientCA, 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -132,11 +108,10 @@ MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
|
|||||||
|
|
||||||
time.Sleep(4 * time.Second)
|
time.Sleep(4 * time.Second)
|
||||||
|
|
||||||
conn2, err := tls.Dial("tcp", apiserverURL.Host, tlsConfig)
|
acceptableCAs, err := cert.GetClientCANamesForURL(kubeconfig.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer conn2.Close()
|
|
||||||
|
|
||||||
expectedCAs := []string{"webhook-test.default.svc", "My Client"}
|
expectedCAs := []string{"webhook-test.default.svc", "My Client"}
|
||||||
if len(expectedCAs) != len(acceptableCAs) {
|
if len(expectedCAs) != len(acceptableCAs) {
|
||||||
@ -334,29 +309,6 @@ func TestServingCert(t *testing.T) {
|
|||||||
dynamiccertificates.FileRefreshDuration = 1 * time.Second
|
dynamiccertificates.FileRefreshDuration = 1 * time.Second
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
apiserverURL, err := url.Parse(kubeconfig.Host)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// when we run this the second time, we know which one we are expecting
|
|
||||||
acceptableCerts := [][]byte{}
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
||||||
acceptableCerts = make([][]byte, 0, len(rawCerts))
|
|
||||||
for _, r := range rawCerts {
|
|
||||||
acceptableCerts = append(acceptableCerts, r)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := tls.Dial("tcp", apiserverURL.Host, tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(path.Join(servingCertPath, "apiserver.key"), serverKey, 0644); err != nil {
|
if err := ioutil.WriteFile(path.Join(servingCertPath, "apiserver.key"), serverKey, 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -367,30 +319,14 @@ func TestServingCert(t *testing.T) {
|
|||||||
|
|
||||||
time.Sleep(4 * time.Second)
|
time.Sleep(4 * time.Second)
|
||||||
|
|
||||||
conn2, err := tls.Dial("tcp", apiserverURL.Host, tlsConfig)
|
// get the certs we're actually serving with
|
||||||
|
_, actualCerts, err := cert.GetServingCertificatesForURL(kubeconfig.Host, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer conn2.Close()
|
if err := checkServingCerts(serverCert, actualCerts); err != nil {
|
||||||
|
|
||||||
cert, err := tls.X509KeyPair(serverCert, serverKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedCerts := cert.Certificate
|
|
||||||
if len(expectedCerts) != len(acceptableCerts) {
|
|
||||||
var certs []string
|
|
||||||
for _, a := range acceptableCerts {
|
|
||||||
certs = append(certs, base64.StdEncoding.EncodeToString(a))
|
|
||||||
}
|
|
||||||
t.Fatalf("Unexpected number of certs: %v", strings.Join(certs, ":"))
|
|
||||||
}
|
|
||||||
for i := range expectedCerts {
|
|
||||||
if !bytes.Equal(acceptableCerts[i], expectedCerts[i]) {
|
|
||||||
t.Errorf("expected %q, got %q", base64.StdEncoding.EncodeToString(expectedCerts[i]), base64.StdEncoding.EncodeToString(acceptableCerts[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSNICert(t *testing.T) {
|
func TestSNICert(t *testing.T) {
|
||||||
@ -419,50 +355,16 @@ func TestSNICert(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
apiserverURL, err := url.Parse(kubeconfig.Host)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we run this the second time, we know which one we are expecting.
|
// When we run this the second time, we know which one we are expecting.
|
||||||
acceptableCerts := [][]byte{}
|
_, actualCerts, err := cert.GetServingCertificatesForURL(kubeconfig.Host, "foo")
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
||||||
acceptableCerts = make([][]byte, 0, len(rawCerts))
|
|
||||||
for _, r := range rawCerts {
|
|
||||||
acceptableCerts = append(acceptableCerts, r)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ServerName: "foo",
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := tls.Dial("tcp", apiserverURL.Host, tlsConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
conn.Close()
|
if err := checkServingCerts(anotherServerCert, actualCerts); err != nil {
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(path.Join(servingCertPath, "foo.crt"), path.Join(servingCertPath, "foo.key"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedCerts := cert.Certificate
|
|
||||||
if len(expectedCerts) != len(acceptableCerts) {
|
|
||||||
var certs []string
|
|
||||||
for _, a := range acceptableCerts {
|
|
||||||
certs = append(certs, base64.StdEncoding.EncodeToString(a))
|
|
||||||
}
|
|
||||||
t.Fatalf("Unexpected number of certs: %v", strings.Join(certs, ":"))
|
|
||||||
}
|
|
||||||
for i := range expectedCerts {
|
|
||||||
if !bytes.Equal(acceptableCerts[i], expectedCerts[i]) {
|
|
||||||
t.Errorf("expected %q, got %q", base64.StdEncoding.EncodeToString(expectedCerts[i]), base64.StdEncoding.EncodeToString(acceptableCerts[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(path.Join(servingCertPath, "foo.key"), serverKey, 0644); err != nil {
|
if err := ioutil.WriteFile(path.Join(servingCertPath, "foo.key"), serverKey, 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -472,28 +374,40 @@ func TestSNICert(t *testing.T) {
|
|||||||
|
|
||||||
time.Sleep(4 * time.Second)
|
time.Sleep(4 * time.Second)
|
||||||
|
|
||||||
conn2, err := tls.Dial("tcp", apiserverURL.Host, tlsConfig)
|
_, actualCerts, err = cert.GetServingCertificatesForURL(kubeconfig.Host, "foo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
conn2.Close()
|
if err := checkServingCerts(serverCert, actualCerts); err != nil {
|
||||||
|
|
||||||
cert, err = tls.X509KeyPair(serverCert, serverKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
expectedCerts = cert.Certificate
|
func checkServingCerts(expectedBytes []byte, actual [][]byte) error {
|
||||||
if len(expectedCerts) != len(acceptableCerts) {
|
expectedCerts, err := cert.ParseCertsPEM(expectedBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
expected := [][]byte{}
|
||||||
|
for _, curr := range expectedCerts {
|
||||||
|
currBytes, err := cert.EncodeCertificates(curr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
expected = append(expected, []byte(strings.TrimSpace(string(currBytes))))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expected) != len(actual) {
|
||||||
var certs []string
|
var certs []string
|
||||||
for _, a := range acceptableCerts {
|
for _, a := range actual {
|
||||||
certs = append(certs, base64.StdEncoding.EncodeToString(a))
|
certs = append(certs, string(a))
|
||||||
}
|
}
|
||||||
t.Fatalf("Unexpected number of certs: %v", strings.Join(certs, ":"))
|
return fmt.Errorf("unexpected number of certs %d vs %d: %v", len(expected), len(actual), strings.Join(certs, "\n"))
|
||||||
}
|
}
|
||||||
for i := range expectedCerts {
|
for i := range expected {
|
||||||
if !bytes.Equal(acceptableCerts[i], expectedCerts[i]) {
|
if !bytes.Equal(actual[i], expected[i]) {
|
||||||
t.Errorf("expected %q, got %q", base64.StdEncoding.EncodeToString(expectedCerts[i]), base64.StdEncoding.EncodeToString(acceptableCerts[i]))
|
return fmt.Errorf("expected %q, got %q", string(expected[i]), string(actual[i]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@ go_test(
|
|||||||
],
|
],
|
||||||
tags = ["integration"],
|
tags = ["integration"],
|
||||||
deps = [
|
deps = [
|
||||||
"//cmd/kube-apiserver/app:go_default_library",
|
|
||||||
"//cmd/kube-apiserver/app/options:go_default_library",
|
"//cmd/kube-apiserver/app/options:go_default_library",
|
||||||
|
"//cmd/kube-apiserver/app/testing:go_default_library",
|
||||||
"//pkg/master:go_default_library",
|
"//pkg/master:go_default_library",
|
||||||
"//pkg/master/reconcilers:go_default_library",
|
"//pkg/master/reconcilers:go_default_library",
|
||||||
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library",
|
"//staging/src/k8s.io/api/admissionregistration/v1beta1:go_default_library",
|
||||||
@ -27,7 +27,7 @@ go_test(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/apis/audit:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/server:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/server/dynamiccertificates:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/discovery:go_default_library",
|
"//staging/src/k8s.io/client-go/discovery:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||||
@ -35,15 +35,12 @@ go_test(
|
|||||||
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
|
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
"//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/util/keyutil:go_default_library",
|
|
||||||
"//staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1:go_default_library",
|
"//staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset:go_default_library",
|
"//staging/src/k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset:go_default_library",
|
||||||
"//staging/src/k8s.io/kube-aggregator/pkg/cmd/server:go_default_library",
|
|
||||||
"//staging/src/k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1:go_default_library",
|
"//staging/src/k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1:go_default_library",
|
||||||
"//staging/src/k8s.io/sample-apiserver/pkg/apis/wardle/v1beta1:go_default_library",
|
"//staging/src/k8s.io/sample-apiserver/pkg/apis/wardle/v1beta1:go_default_library",
|
||||||
"//staging/src/k8s.io/sample-apiserver/pkg/cmd/server:go_default_library",
|
"//staging/src/k8s.io/sample-apiserver/pkg/cmd/server:go_default_library",
|
||||||
"//test/integration/framework:go_default_library",
|
"//test/integration/framework:go_default_library",
|
||||||
"//test/utils:go_default_library",
|
|
||||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -25,7 +24,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync/atomic"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -35,293 +34,82 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
|
genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
|
||||||
discovery "k8s.io/client-go/discovery"
|
"k8s.io/client-go/discovery"
|
||||||
client "k8s.io/client-go/kubernetes"
|
client "k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
"k8s.io/client-go/util/cert"
|
"k8s.io/client-go/util/cert"
|
||||||
"k8s.io/client-go/util/keyutil"
|
|
||||||
apiregistrationv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
|
apiregistrationv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
|
||||||
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
|
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
|
||||||
kubeaggregatorserver "k8s.io/kube-aggregator/pkg/cmd/server"
|
kastesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
"k8s.io/kubernetes/cmd/kube-apiserver/app"
|
|
||||||
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
testutil "k8s.io/kubernetes/test/utils"
|
|
||||||
wardlev1alpha1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1"
|
wardlev1alpha1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1"
|
||||||
wardlev1beta1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1beta1"
|
wardlev1beta1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1beta1"
|
||||||
sampleserver "k8s.io/sample-apiserver/pkg/cmd/server"
|
sampleserver "k8s.io/sample-apiserver/pkg/cmd/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAggregatedAPIServer(t *testing.T) {
|
func TestAggregatedAPIServer(t *testing.T) {
|
||||||
|
// makes the kube-apiserver very responsive. it's normally a minute
|
||||||
|
dynamiccertificates.FileRefreshDuration = 1 * time.Second
|
||||||
|
|
||||||
stopCh := make(chan struct{})
|
stopCh := make(chan struct{})
|
||||||
defer close(stopCh)
|
defer close(stopCh)
|
||||||
|
|
||||||
certDir, _ := ioutil.TempDir("", "test-integration-apiserver")
|
testServer := kastesting.StartTestServerOrDie(t, &kastesting.TestServerInstanceOptions{EnableCertAuth: true}, nil, framework.SharedEtcd())
|
||||||
defer os.RemoveAll(certDir)
|
defer testServer.TearDownFn()
|
||||||
_, defaultServiceClusterIPRange, _ := net.ParseCIDR("10.0.0.0/24")
|
kubeClientConfig := rest.CopyConfig(testServer.ClientConfig)
|
||||||
proxySigningKey, err := testutil.NewPrivateKey()
|
// force json because everything speaks it
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
proxySigningCert, err := cert.NewSelfSignedCACert(cert.Config{CommonName: "front-proxy-ca"}, proxySigningKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
proxyCACertFile, _ := ioutil.TempFile(certDir, "proxy-ca.crt")
|
|
||||||
if err := ioutil.WriteFile(proxyCACertFile.Name(), testutil.EncodeCertPEM(proxySigningCert), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
clientSigningKey, err := testutil.NewPrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
clientSigningCert, err := cert.NewSelfSignedCACert(cert.Config{CommonName: "client-ca"}, clientSigningKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
clientCACertFile, _ := ioutil.TempFile(certDir, "client-ca.crt")
|
|
||||||
if err := ioutil.WriteFile(clientCACertFile.Name(), testutil.EncodeCertPEM(clientSigningCert), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
kubeClientConfigValue := atomic.Value{}
|
|
||||||
go func() {
|
|
||||||
listener, _, err := genericapiserveroptions.CreateListener("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
kubeAPIServerOptions := options.NewServerRunOptions()
|
|
||||||
kubeAPIServerOptions.SecureServing.Listener = listener
|
|
||||||
kubeAPIServerOptions.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
|
|
||||||
kubeAPIServerOptions.SecureServing.ServerCert.CertDirectory = certDir
|
|
||||||
kubeAPIServerOptions.InsecureServing.BindPort = 0
|
|
||||||
kubeAPIServerOptions.Etcd.StorageConfig.Transport.ServerList = []string{framework.GetEtcdURL()}
|
|
||||||
kubeAPIServerOptions.ServiceClusterIPRanges = defaultServiceClusterIPRange.String()
|
|
||||||
kubeAPIServerOptions.Authentication.RequestHeader.UsernameHeaders = []string{"X-Remote-User"}
|
|
||||||
kubeAPIServerOptions.Authentication.RequestHeader.GroupHeaders = []string{"X-Remote-Group"}
|
|
||||||
kubeAPIServerOptions.Authentication.RequestHeader.ExtraHeaderPrefixes = []string{"X-Remote-Extra-"}
|
|
||||||
kubeAPIServerOptions.Authentication.RequestHeader.AllowedNames = []string{"kube-aggregator"}
|
|
||||||
kubeAPIServerOptions.Authentication.RequestHeader.ClientCAFile = proxyCACertFile.Name()
|
|
||||||
kubeAPIServerOptions.Authentication.ClientCert.ClientCA = clientCACertFile.Name()
|
|
||||||
kubeAPIServerOptions.Authorization.Modes = []string{"RBAC"}
|
|
||||||
completedOptions, err := app.Complete(kubeAPIServerOptions)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tunneler, proxyTransport, err := app.CreateNodeDialer(completedOptions)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
kubeAPIServerConfig, _, _, _, err := app.CreateKubeAPIServerConfig(completedOptions, tunneler, proxyTransport)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// Adjust the loopback config for external use (external server name and CA)
|
|
||||||
kubeAPIServerClientConfig := rest.CopyConfig(kubeAPIServerConfig.GenericConfig.LoopbackClientConfig)
|
|
||||||
kubeAPIServerClientConfig.CAFile = path.Join(certDir, "apiserver.crt")
|
|
||||||
kubeAPIServerClientConfig.CAData = nil
|
|
||||||
kubeAPIServerClientConfig.ServerName = ""
|
|
||||||
kubeClientConfigValue.Store(kubeAPIServerClientConfig)
|
|
||||||
|
|
||||||
kubeAPIServer, err := app.CreateKubeAPIServer(kubeAPIServerConfig, genericapiserver.NewEmptyDelegate())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := kubeAPIServer.GenericAPIServer.PrepareRun().Run(wait.NeverStop); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// just use json because everyone speaks it
|
|
||||||
err = wait.PollImmediate(time.Second, time.Minute, func() (done bool, err error) {
|
|
||||||
obj := kubeClientConfigValue.Load()
|
|
||||||
if obj == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
kubeClientConfig := kubeClientConfigValue.Load().(*rest.Config)
|
|
||||||
kubeClientConfig.ContentType = ""
|
kubeClientConfig.ContentType = ""
|
||||||
kubeClientConfig.AcceptContentTypes = ""
|
kubeClientConfig.AcceptContentTypes = ""
|
||||||
kubeClient, err := client.NewForConfig(kubeClientConfig)
|
kubeClient := client.NewForConfigOrDie(kubeClientConfig)
|
||||||
if err != nil {
|
aggregatorClient := aggregatorclient.NewForConfigOrDie(kubeClientConfig)
|
||||||
// this happens because we race the API server start
|
|
||||||
t.Log(err)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
healthStatus := 0
|
|
||||||
kubeClient.Discovery().RESTClient().Get().AbsPath("/healthz").Do().StatusCode(&healthStatus)
|
|
||||||
if healthStatus != http.StatusOK {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// after this point we won't be mutating, so the race detector will be fine
|
|
||||||
kubeClientConfig := kubeClientConfigValue.Load().(*rest.Config)
|
|
||||||
|
|
||||||
// write a kubeconfig out for starting other API servers with delegated auth. remember, no in-cluster config
|
|
||||||
adminKubeConfig := createKubeConfig(kubeClientConfig)
|
|
||||||
kubeconfigFile, _ := ioutil.TempFile("", "")
|
|
||||||
defer os.Remove(kubeconfigFile.Name())
|
|
||||||
clientcmd.WriteToFile(*adminKubeConfig, kubeconfigFile.Name())
|
|
||||||
wardleCertDir, _ := ioutil.TempDir("", "test-integration-wardle-server")
|
|
||||||
defer os.RemoveAll(wardleCertDir)
|
|
||||||
wardlePort := new(int32)
|
|
||||||
|
|
||||||
// start the wardle server to prove we can aggregate it
|
// start the wardle server to prove we can aggregate it
|
||||||
go func() {
|
wardleToKASKubeConfigFile := writeKubeConfigForWardleServerToKASConnection(t, rest.CopyConfig(kubeClientConfig))
|
||||||
listener, port, err := genericapiserveroptions.CreateListener("tcp", "127.0.0.1:0")
|
defer os.Remove(wardleToKASKubeConfigFile)
|
||||||
|
wardleCertDir, _ := ioutil.TempDir("", "test-integration-wardle-server")
|
||||||
|
defer os.RemoveAll(wardleCertDir)
|
||||||
|
listener, wardlePort, err := genericapiserveroptions.CreateListener("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
atomic.StoreInt32(wardlePort, int32(port))
|
go func() {
|
||||||
|
|
||||||
o := sampleserver.NewWardleServerOptions(os.Stdout, os.Stderr)
|
o := sampleserver.NewWardleServerOptions(os.Stdout, os.Stderr)
|
||||||
o.RecommendedOptions.SecureServing.Listener = listener
|
o.RecommendedOptions.SecureServing.Listener = listener
|
||||||
o.RecommendedOptions.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
|
o.RecommendedOptions.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
|
||||||
wardleCmd := sampleserver.NewCommandStartWardleServer(o, stopCh)
|
wardleCmd := sampleserver.NewCommandStartWardleServer(o, stopCh)
|
||||||
wardleCmd.SetArgs([]string{
|
wardleCmd.SetArgs([]string{
|
||||||
"--requestheader-username-headers=X-Remote-User",
|
"--authentication-kubeconfig", wardleToKASKubeConfigFile,
|
||||||
"--requestheader-group-headers=X-Remote-Group",
|
"--authorization-kubeconfig", wardleToKASKubeConfigFile,
|
||||||
"--requestheader-extra-headers-prefix=X-Remote-Extra-",
|
|
||||||
"--requestheader-client-ca-file=" + proxyCACertFile.Name(),
|
|
||||||
"--requestheader-allowed-names=kube-aggregator",
|
|
||||||
"--authentication-kubeconfig", kubeconfigFile.Name(),
|
|
||||||
"--authorization-kubeconfig", kubeconfigFile.Name(),
|
|
||||||
"--etcd-servers", framework.GetEtcdURL(),
|
"--etcd-servers", framework.GetEtcdURL(),
|
||||||
"--cert-dir", wardleCertDir,
|
"--cert-dir", wardleCertDir,
|
||||||
"--kubeconfig", kubeconfigFile.Name(),
|
"--kubeconfig", wardleToKASKubeConfigFile,
|
||||||
})
|
})
|
||||||
if err := wardleCmd.Execute(); err != nil {
|
if err := wardleCmd.Execute(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
directWardleClientConfig, err := waitForWardleRunning(t, kubeClientConfig, wardleCertDir, wardlePort)
|
||||||
wardleClientConfig := rest.AnonymousClientConfig(kubeClientConfig)
|
|
||||||
wardleClientConfig.CAFile = path.Join(wardleCertDir, "apiserver.crt")
|
|
||||||
wardleClientConfig.CAData = nil
|
|
||||||
wardleClientConfig.ServerName = ""
|
|
||||||
wardleClientConfig.BearerToken = kubeClientConfig.BearerToken
|
|
||||||
var wardleClient client.Interface
|
|
||||||
err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) {
|
|
||||||
wardleClientConfig.Host = fmt.Sprintf("https://127.0.0.1:%d", atomic.LoadInt32(wardlePort))
|
|
||||||
wardleClient, err = client.NewForConfig(wardleClientConfig)
|
|
||||||
if err != nil {
|
|
||||||
// this happens because we race the API server start
|
|
||||||
t.Log(err)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
healthStatus := 0
|
|
||||||
wardleClient.Discovery().RESTClient().Get().AbsPath("/healthz").Do().StatusCode(&healthStatus)
|
|
||||||
if healthStatus != http.StatusOK {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start the aggregator
|
|
||||||
aggregatorCertDir, _ := ioutil.TempDir("", "test-integration-aggregator")
|
|
||||||
defer os.RemoveAll(aggregatorCertDir)
|
|
||||||
proxyClientKey, err := testutil.NewPrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
proxyClientCert, err := testutil.NewSignedCert(
|
|
||||||
&cert.Config{
|
|
||||||
CommonName: "kube-aggregator",
|
|
||||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
||||||
},
|
|
||||||
proxyClientKey, proxySigningCert, proxySigningKey,
|
|
||||||
)
|
|
||||||
proxyClientCertFile, _ := ioutil.TempFile(aggregatorCertDir, "proxy-client.crt")
|
|
||||||
proxyClientKeyFile, _ := ioutil.TempFile(aggregatorCertDir, "proxy-client.key")
|
|
||||||
if err := ioutil.WriteFile(proxyClientCertFile.Name(), testutil.EncodeCertPEM(proxyClientCert), 0600); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
proxyClientKeyPEM, err := keyutil.MarshalPrivateKeyToPEM(proxyClientKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := ioutil.WriteFile(proxyClientKeyFile.Name(), proxyClientKeyPEM, 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
aggregatorPort := new(int32)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
listener, port, err := genericapiserveroptions.CreateListener("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
atomic.StoreInt32(aggregatorPort, int32(port))
|
|
||||||
|
|
||||||
o := kubeaggregatorserver.NewDefaultOptions(os.Stdout, os.Stderr)
|
|
||||||
o.RecommendedOptions.SecureServing.Listener = listener
|
|
||||||
o.RecommendedOptions.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
|
|
||||||
aggregatorCmd := kubeaggregatorserver.NewCommandStartAggregator(o, stopCh)
|
|
||||||
aggregatorCmd.SetArgs([]string{
|
|
||||||
"--requestheader-username-headers", "",
|
|
||||||
"--proxy-client-cert-file", proxyClientCertFile.Name(),
|
|
||||||
"--proxy-client-key-file", proxyClientKeyFile.Name(),
|
|
||||||
"--kubeconfig", kubeconfigFile.Name(),
|
|
||||||
"--authentication-kubeconfig", kubeconfigFile.Name(),
|
|
||||||
"--authorization-kubeconfig", kubeconfigFile.Name(),
|
|
||||||
"--etcd-servers", framework.GetEtcdURL(),
|
|
||||||
"--cert-dir", aggregatorCertDir,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := aggregatorCmd.Execute(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
aggregatorClientConfig := rest.AnonymousClientConfig(kubeClientConfig)
|
|
||||||
aggregatorClientConfig.CAFile = path.Join(aggregatorCertDir, "apiserver.crt")
|
|
||||||
aggregatorClientConfig.CAData = nil
|
|
||||||
aggregatorClientConfig.ServerName = ""
|
|
||||||
aggregatorClientConfig.BearerToken = kubeClientConfig.BearerToken
|
|
||||||
var aggregatorDiscoveryClient client.Interface
|
|
||||||
err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) {
|
|
||||||
aggregatorClientConfig.Host = fmt.Sprintf("https://127.0.0.1:%d", atomic.LoadInt32(aggregatorPort))
|
|
||||||
aggregatorDiscoveryClient, err = client.NewForConfig(aggregatorClientConfig)
|
|
||||||
if err != nil {
|
|
||||||
// this happens if we race the API server for writing the cert
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
healthStatus := 0
|
|
||||||
aggregatorDiscoveryClient.Discovery().RESTClient().Get().AbsPath("/healthz").Do().StatusCode(&healthStatus)
|
|
||||||
if healthStatus != http.StatusOK {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// now we're finally ready to test. These are what's run by default now
|
// now we're finally ready to test. These are what's run by default now
|
||||||
|
wardleClient, err := client.NewForConfig(directWardleClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
testAPIGroupList(t, wardleClient.Discovery().RESTClient())
|
testAPIGroupList(t, wardleClient.Discovery().RESTClient())
|
||||||
testAPIGroup(t, wardleClient.Discovery().RESTClient())
|
testAPIGroup(t, wardleClient.Discovery().RESTClient())
|
||||||
testAPIResourceList(t, wardleClient.Discovery().RESTClient())
|
testAPIResourceList(t, wardleClient.Discovery().RESTClient())
|
||||||
|
|
||||||
wardleCA, err := ioutil.ReadFile(wardleClientConfig.CAFile)
|
wardleCA, err := ioutil.ReadFile(directWardleClientConfig.CAFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
aggregatorClient := aggregatorclient.NewForConfigOrDie(aggregatorClientConfig)
|
|
||||||
_, err = aggregatorClient.ApiregistrationV1beta1().APIServices().Create(&apiregistrationv1beta1.APIService{
|
_, err = aggregatorClient.ApiregistrationV1beta1().APIServices().Create(&apiregistrationv1beta1.APIService{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "v1alpha1.wardle.example.com"},
|
ObjectMeta: metav1.ObjectMeta{Name: "v1alpha1.wardle.example.com"},
|
||||||
Spec: apiregistrationv1beta1.APIServiceSpec{
|
Spec: apiregistrationv1beta1.APIServiceSpec{
|
||||||
@ -342,39 +130,154 @@ func TestAggregatedAPIServer(t *testing.T) {
|
|||||||
|
|
||||||
// wait for the unavailable API service to be processed with updated status
|
// wait for the unavailable API service to be processed with updated status
|
||||||
err = wait.Poll(100*time.Millisecond, 5*time.Second, func() (done bool, err error) {
|
err = wait.Poll(100*time.Millisecond, 5*time.Second, func() (done bool, err error) {
|
||||||
_, err = aggregatorDiscoveryClient.Discovery().ServerResources()
|
_, err = kubeClient.Discovery().ServerResources()
|
||||||
hasExpectedError := checkWardleUnavailableDiscoveryError(t, err)
|
hasExpectedError := checkWardleUnavailableDiscoveryError(t, err)
|
||||||
return hasExpectedError, nil
|
return hasExpectedError, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
// TODO figure out how to turn on enough of services and dns to run more
|
||||||
|
|
||||||
_, err = aggregatorClient.ApiregistrationV1beta1().APIServices().Create(&apiregistrationv1beta1.APIService{
|
// Now we want to verify that the client CA bundles properly reflect the values for the cluster-authentication
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "v1."},
|
firstKubeCANames, err := cert.GetClientCANamesForURL(kubeClientConfig.Host)
|
||||||
Spec: apiregistrationv1beta1.APIServiceSpec{
|
|
||||||
// register this as a local service so it doesn't try to lookup the default kubernetes service
|
|
||||||
// which will have an unroutable IP address since it's fake.
|
|
||||||
Group: "",
|
|
||||||
Version: "v1",
|
|
||||||
GroupPriorityMinimum: 100,
|
|
||||||
VersionPriority: 100,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
t.Log(firstKubeCANames)
|
||||||
// this is ugly, but sleep just a little bit so that the watch is probably observed. Since nothing will actually be added to discovery
|
firstWardleCANames, err := cert.GetClientCANamesForURL(directWardleClientConfig.Host)
|
||||||
// (the service is missing), we don't have an external signal.
|
if err != nil {
|
||||||
time.Sleep(100 * time.Millisecond)
|
t.Fatal(err)
|
||||||
_, err = aggregatorDiscoveryClient.Discovery().ServerResources()
|
}
|
||||||
hasExpectedError := checkWardleUnavailableDiscoveryError(t, err)
|
t.Log(firstWardleCANames)
|
||||||
if !hasExpectedError {
|
if !reflect.DeepEqual(firstKubeCANames, firstWardleCANames) {
|
||||||
t.Fatalf("Discovery call didn't return expected error: %v", err)
|
t.Fatal("names don't match")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO figure out how to turn on enough of services and dns to run more
|
// now we update the client-ca nd request-header-client-ca-file and the kas will consume it, update the configmap
|
||||||
|
// and then the wardle server will detect and update too.
|
||||||
|
if err := ioutil.WriteFile(path.Join(testServer.TmpDir, "client-ca.crt"), differentClientCA, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(path.Join(testServer.TmpDir, "proxy-ca.crt"), differentFrontProxyCA, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// wait for it to be picked up. there's a test in certreload_test.go that ensure this works
|
||||||
|
time.Sleep(4 * time.Second)
|
||||||
|
|
||||||
|
// Now we want to verify that the client CA bundles properly updated to reflect the new values written for the kube-apiserver
|
||||||
|
secondKubeCANames, err := cert.GetClientCANamesForURL(kubeClientConfig.Host)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(secondKubeCANames)
|
||||||
|
for i := range firstKubeCANames {
|
||||||
|
if firstKubeCANames[i] == secondKubeCANames[i] {
|
||||||
|
t.Errorf("ca bundles should change")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secondWardleCANames, err := cert.GetClientCANamesForURL(directWardleClientConfig.Host)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Log(secondWardleCANames)
|
||||||
|
|
||||||
|
// second wardle should contain all the certs, first and last
|
||||||
|
numMatches := 0
|
||||||
|
for _, needle := range firstKubeCANames {
|
||||||
|
for _, haystack := range secondWardleCANames {
|
||||||
|
if needle == haystack {
|
||||||
|
numMatches++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, needle := range secondKubeCANames {
|
||||||
|
for _, haystack := range secondWardleCANames {
|
||||||
|
if needle == haystack {
|
||||||
|
numMatches++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if numMatches != 4 {
|
||||||
|
t.Fatal("names don't match")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForWardleRunning(t *testing.T, wardleToKASKubeConfig *rest.Config, wardleCertDir string, wardlePort int) (*rest.Config, error) {
|
||||||
|
directWardleClientConfig := rest.AnonymousClientConfig(rest.CopyConfig(wardleToKASKubeConfig))
|
||||||
|
directWardleClientConfig.CAFile = path.Join(wardleCertDir, "apiserver.crt")
|
||||||
|
directWardleClientConfig.CAData = nil
|
||||||
|
directWardleClientConfig.ServerName = ""
|
||||||
|
directWardleClientConfig.BearerToken = wardleToKASKubeConfig.BearerToken
|
||||||
|
var wardleClient client.Interface
|
||||||
|
lastHealthContent := []byte{}
|
||||||
|
var lastHealthErr error
|
||||||
|
err := wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) {
|
||||||
|
if _, err := os.Stat(directWardleClientConfig.CAFile); os.IsNotExist(err) { // wait until the file trust is created
|
||||||
|
lastHealthErr = err
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
directWardleClientConfig.Host = fmt.Sprintf("https://127.0.0.1:%d", wardlePort)
|
||||||
|
wardleClient, err = client.NewForConfig(directWardleClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
// this happens because we race the API server start
|
||||||
|
t.Log(err)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
healthStatus := 0
|
||||||
|
result := wardleClient.Discovery().RESTClient().Get().AbsPath("/healthz").Do().StatusCode(&healthStatus)
|
||||||
|
lastHealthContent, lastHealthErr = result.Raw()
|
||||||
|
if healthStatus != http.StatusOK {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Log(string(lastHealthContent))
|
||||||
|
t.Log(lastHealthErr)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return directWardleClientConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeKubeConfigForWardleServerToKASConnection(t *testing.T, kubeClientConfig *rest.Config) string {
|
||||||
|
// write a kubeconfig out for starting other API servers with delegated auth. remember, no in-cluster config
|
||||||
|
// the loopback client config uses a loopback cert with different SNI. We need to use the "real"
|
||||||
|
// cert, so we'll hope we aren't hacked during a unit test and instead load it from the server we started.
|
||||||
|
wardleToKASKubeClientConfig := rest.CopyConfig(kubeClientConfig)
|
||||||
|
|
||||||
|
servingCerts, _, err := cert.GetServingCertificatesForURL(wardleToKASKubeClientConfig.Host, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
encodedServing, err := cert.EncodeCertificates(servingCerts...)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wardleToKASKubeClientConfig.CAData = encodedServing
|
||||||
|
|
||||||
|
for _, v := range servingCerts {
|
||||||
|
t.Logf("Client: Server public key is %v\n", dynamiccertificates.GetHumanCertDetail(v))
|
||||||
|
}
|
||||||
|
certs, err := cert.ParseCertsPEM(wardleToKASKubeClientConfig.CAData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, curr := range certs {
|
||||||
|
t.Logf("CA bundle %v\n", dynamiccertificates.GetHumanCertDetail(curr))
|
||||||
|
}
|
||||||
|
|
||||||
|
adminKubeConfig := createKubeConfig(wardleToKASKubeClientConfig)
|
||||||
|
wardleToKASKubeConfigFile, _ := ioutil.TempFile("", "")
|
||||||
|
if err := clientcmd.WriteToFile(*adminKubeConfig, wardleToKASKubeConfigFile.Name()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wardleToKASKubeConfigFile.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkWardleUnavailableDiscoveryError(t *testing.T, err error) bool {
|
func checkWardleUnavailableDiscoveryError(t *testing.T, err error) bool {
|
||||||
@ -510,3 +413,41 @@ func testAPIResourceList(t *testing.T, client rest.Interface) {
|
|||||||
assert.Equal(t, "flunders", apiResourceList.APIResources[1].Name)
|
assert.Equal(t, "flunders", apiResourceList.APIResources[1].Name)
|
||||||
assert.True(t, apiResourceList.APIResources[1].Namespaced)
|
assert.True(t, apiResourceList.APIResources[1].Namespaced)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// I have no idea what these certs are, they just need to be different
|
||||||
|
differentClientCA = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDQDCCAiigAwIBAgIJANWw74P5KJk2MA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
|
||||||
|
BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
|
||||||
|
DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjAjMSEwHwYDVQQDExh3ZWJo
|
||||||
|
b29rLXRlc3QuZGVmYXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||||
|
AoIBAQDXd/nQ89a5H8ifEsigmMd01Ib6NVR3bkJjtkvYnTbdfYEBj7UzqOQtHoLa
|
||||||
|
dIVmefny5uIHvj93WD8WDVPB3jX2JHrXkDTXd/6o6jIXHcsUfFTVLp6/bZ+Anqe0
|
||||||
|
r/7hAPkzA2A7APyTWM3ZbEeo1afXogXhOJ1u/wz0DflgcB21gNho4kKTONXO3NHD
|
||||||
|
XLpspFqSkxfEfKVDJaYAoMnYZJtFNsa2OvsmLnhYF8bjeT3i07lfwrhUZvP+7Gsp
|
||||||
|
7UgUwc06WuNHjfx1s5e6ySzH0QioMD1rjYneqOvk0pKrMIhuAEWXqq7jlXcDtx1E
|
||||||
|
j+wnYbVqqVYheHZ8BCJoVAAQGs9/AgMBAAGjZDBiMAkGA1UdEwQCMAAwCwYDVR0P
|
||||||
|
BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATApBgNVHREEIjAg
|
||||||
|
hwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVsdC5zdmMwDQYJKoZIhvcNAQELBQAD
|
||||||
|
ggEBAD/GKSPNyQuAOw/jsYZesb+RMedbkzs18sSwlxAJQMUrrXwlVdHrA8q5WhE6
|
||||||
|
ABLqU1b8lQ8AWun07R8k5tqTmNvCARrAPRUqls/ryER+3Y9YEcxEaTc3jKNZFLbc
|
||||||
|
T6YtcnkdhxsiO136wtiuatpYL91RgCmuSpR8+7jEHhuFU01iaASu7ypFrUzrKHTF
|
||||||
|
bKwiLRQi1cMzVcLErq5CDEKiKhUkoDucyARFszrGt9vNIl/YCcBOkcNvM3c05Hn3
|
||||||
|
M++C29JwS3Hwbubg6WO3wjFjoEhpCwU6qRYUz3MRp4tHO4kxKXx+oQnUiFnR7vW0
|
||||||
|
YkNtGc1RUDHwecCTFpJtPb7Yu/E=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`)
|
||||||
|
differentFrontProxyCA = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
|
||||||
|
GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
|
||||||
|
MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
|
||||||
|
BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
|
||||||
|
KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
|
||||||
|
BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
|
||||||
|
K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
|
||||||
|
a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
|
||||||
|
MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -1711,7 +1711,6 @@ k8s.io/kube-aggregator/pkg/client/informers/externalversions/apiregistration/v1b
|
|||||||
k8s.io/kube-aggregator/pkg/client/informers/externalversions/internalinterfaces
|
k8s.io/kube-aggregator/pkg/client/informers/externalversions/internalinterfaces
|
||||||
k8s.io/kube-aggregator/pkg/client/listers/apiregistration/v1
|
k8s.io/kube-aggregator/pkg/client/listers/apiregistration/v1
|
||||||
k8s.io/kube-aggregator/pkg/client/listers/apiregistration/v1beta1
|
k8s.io/kube-aggregator/pkg/client/listers/apiregistration/v1beta1
|
||||||
k8s.io/kube-aggregator/pkg/cmd/server
|
|
||||||
k8s.io/kube-aggregator/pkg/controllers
|
k8s.io/kube-aggregator/pkg/controllers
|
||||||
k8s.io/kube-aggregator/pkg/controllers/autoregister
|
k8s.io/kube-aggregator/pkg/controllers/autoregister
|
||||||
k8s.io/kube-aggregator/pkg/controllers/openapi
|
k8s.io/kube-aggregator/pkg/controllers/openapi
|
||||||
|
Loading…
Reference in New Issue
Block a user