From 3b88b4aa63861a371fff216b97069b0bcc125b54 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Thu, 28 Sep 2017 23:25:06 -0400 Subject: [PATCH 01/11] UPSTREAM: 49899: Update the client cert used by the kubelet on expiry --- .../kubernetes/cmd/kubelet/app/server.go | 41 +--- .../pkg/kubelet/certificate/transport.go | 168 +++++++++++++++ .../pkg/kubelet/certificate/transport_test.go | 191 ++++++++++++++++++ 3 files changed, 360 insertions(+), 40 deletions(-) create mode 100644 vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport.go create mode 100644 vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport_test.go diff --git a/vendor/k8s.io/kubernetes/cmd/kubelet/app/server.go b/vendor/k8s.io/kubernetes/cmd/kubelet/app/server.go index c52d7fd0cb1a..d1d6cf4ec08d 100644 --- a/vendor/k8s.io/kubernetes/cmd/kubelet/app/server.go +++ b/vendor/k8s.io/kubernetes/cmd/kubelet/app/server.go @@ -39,7 +39,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - utilnet "k8s.io/apimachinery/pkg/util/net" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" @@ -467,7 +466,7 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.KubeletDeps) (err error) { if err != nil { return err } - if err := updateTransport(clientConfig, clientCertificateManager); err != nil { + if err := certificate.UpdateTransport(wait.NeverStop, clientConfig, clientCertificateManager); err != nil { return err } } @@ -629,44 +628,6 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.KubeletDeps) (err error) { return nil } -func updateTransport(clientConfig *restclient.Config, clientCertificateManager certificate.Manager) error { - if clientConfig.Transport != nil { - return fmt.Errorf("there is already a transport configured") - } - tlsConfig, err := restclient.TLSConfigFor(clientConfig) - if err != nil { - return fmt.Errorf("unable to configure TLS for the rest client: %v", err) - } - if tlsConfig == nil { - tlsConfig = &tls.Config{} - } - tlsConfig.Certificates = nil - tlsConfig.GetClientCertificate = func(requestInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) { - cert := clientCertificateManager.Current() - if cert == nil { - return &tls.Certificate{Certificate: nil}, nil - } - return cert, nil - } - clientConfig.Transport = utilnet.SetTransportDefaults(&http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: tlsConfig, - MaxIdleConnsPerHost: 25, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - }) - clientConfig.CertData = nil - clientConfig.KeyData = nil - clientConfig.CertFile = "" - clientConfig.KeyFile = "" - clientConfig.CAData = nil - clientConfig.CAFile = "" - return nil -} - // getNodeName returns the node name according to the cloud provider // if cloud provider is specified. Otherwise, returns the hostname of the node. func getNodeName(cloud cloudprovider.Interface, hostname string) (types.NodeName, error) { diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport.go b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport.go new file mode 100644 index 000000000000..bb472039f3fb --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport.go @@ -0,0 +1,168 @@ +/* +Copyright 2017 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 certificate + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "sync" + "time" + + "github.com/golang/glog" + + utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apimachinery/pkg/util/wait" + restclient "k8s.io/client-go/rest" +) + +// UpdateTransport instruments a restconfig with a transport that dynamically uses +// certificates provided by the manager for TLS client auth. +// +// The config must not already provide an explicit transport. +// +// The returned transport periodically checks the manager to determine if the +// certificate has changed. If it has, the transport shuts down all existing client +// connections, forcing the client to re-handshake with the server and use the +// new certificate. +// +// stopCh should be used to indicate when the transport is unused and doesn't need +// to continue checking the manager. +func UpdateTransport(stopCh <-chan struct{}, clientConfig *restclient.Config, clientCertificateManager Manager) error { + return updateTransport(stopCh, 10*time.Second, clientConfig, clientCertificateManager) +} + +// updateTransport is an internal method that exposes how often this method checks that the +// client cert has changed. Intended for testing. +func updateTransport(stopCh <-chan struct{}, period time.Duration, clientConfig *restclient.Config, clientCertificateManager Manager) error { + if clientConfig.Transport != nil { + return fmt.Errorf("there is already a transport configured") + } + tlsConfig, err := restclient.TLSConfigFor(clientConfig) + if err != nil { + return fmt.Errorf("unable to configure TLS for the rest client: %v", err) + } + if tlsConfig == nil { + tlsConfig = &tls.Config{} + } + tlsConfig.Certificates = nil + tlsConfig.GetClientCertificate = func(requestInfo *tls.CertificateRequestInfo) (*tls.Certificate, error) { + cert := clientCertificateManager.Current() + if cert == nil { + return &tls.Certificate{Certificate: nil}, nil + } + return cert, nil + } + + // Custom dialer that will track all connections it creates. + t := &connTracker{ + dialer: &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}, + conns: make(map[*closableConn]struct{}), + } + + lastCert := clientCertificateManager.Current() + go wait.Until(func() { + curr := clientCertificateManager.Current() + if curr == nil || lastCert == curr { + // Cert hasn't been rotated. + return + } + lastCert = curr + + glog.Infof("certificate rotation detected, shutting down client connections to start using new credentials") + // The cert has been rotated. Close all existing connections to force the client + // to reperform its TLS handshake with new cert. + // + // See: https://github.com/kubernetes-incubator/bootkube/pull/663#issuecomment-318506493 + t.closeAllConns() + }, period, stopCh) + + clientConfig.Transport = utilnet.SetTransportDefaults(&http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: tlsConfig, + MaxIdleConnsPerHost: 25, + DialContext: t.DialContext, // Use custom dialer. + }) + + // Zero out all existing TLS options since our new transport enforces them. + clientConfig.CertData = nil + clientConfig.KeyData = nil + clientConfig.CertFile = "" + clientConfig.KeyFile = "" + clientConfig.CAData = nil + clientConfig.CAFile = "" + clientConfig.Insecure = false + return nil +} + +// connTracker is a dialer that tracks all open connections it creates. +type connTracker struct { + dialer *net.Dialer + + mu sync.Mutex + conns map[*closableConn]struct{} +} + +// closeAllConns forcibly closes all tracked connections. +func (c *connTracker) closeAllConns() { + c.mu.Lock() + conns := c.conns + c.conns = make(map[*closableConn]struct{}) + c.mu.Unlock() + + for conn := range conns { + conn.Close() + } +} + +func (c *connTracker) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + conn, err := c.dialer.DialContext(ctx, network, address) + if err != nil { + return nil, err + } + + closable := &closableConn{Conn: conn} + + // Start tracking the connection + c.mu.Lock() + c.conns[closable] = struct{}{} + c.mu.Unlock() + + // When the connection is closed, remove it from the map. This will + // be no-op if the connection isn't in the map, e.g. if closeAllConns() + // is called. + closable.onClose = func() { + c.mu.Lock() + delete(c.conns, closable) + c.mu.Unlock() + } + + return closable, nil +} + +type closableConn struct { + onClose func() + net.Conn +} + +func (c *closableConn) Close() error { + go c.onClose() + return c.Conn.Close() +} diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport_test.go b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport_test.go new file mode 100644 index 000000000000..1067f79c9d93 --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport_test.go @@ -0,0 +1,191 @@ +/* +Copyright 2017 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 certificate + +import ( + "crypto/tls" + "crypto/x509" + "math/big" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" + certificatesclient "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/certificates/v1beta1" +) + +var ( + client1CertData = newCertificateData(`-----BEGIN CERTIFICATE----- +MIICBDCCAW2gAwIBAgIJAPgVBh+4xbGoMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAMMEHdlYmhvb2tfdGVzdHNfY2EwIBcNMTcwNzI4MjMxNTI4WhgPMjI5MTA1MTMy +MzE1MjhaMB8xHTAbBgNVBAMMFHdlYmhvb2tfdGVzdHNfY2xpZW50MIGfMA0GCSqG +SIb3DQEBAQUAA4GNADCBiQKBgQDkGXXSm6Yun5o3Jlmx45rItcQ2pmnoDk4eZfl0 +rmPa674s2pfYo3KywkXQ1Fp3BC8GUgzPLSfJ8xXya9Lg1Wo8sHrDln0iRg5HXxGu +uFNhRBvj2S0sIff0ZG/IatB9I6WXVOUYuQj6+A0CdULNj1vBqH9+7uWbLZ6lrD4b +a44x/wIDAQABo0owSDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAU +BggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0B +AQsFAAOBgQCpN27uh/LjUVCaBK7Noko25iih/JSSoWzlvc8CaipvSPofNWyGx3Vu +OdcSwNGYX/pp4ZoAzFij/Y5u0vKTVLkWXATeTMVmlPvhmpYjj9gPkCSY6j/SiKlY +kGy0xr+0M5UQkMBcfIh9oAp9um1fZHVWAJAGP/ikZgkcUey0LmBn8w== +-----END CERTIFICATE-----`, `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDkGXXSm6Yun5o3Jlmx45rItcQ2pmnoDk4eZfl0rmPa674s2pfY +o3KywkXQ1Fp3BC8GUgzPLSfJ8xXya9Lg1Wo8sHrDln0iRg5HXxGuuFNhRBvj2S0s +Iff0ZG/IatB9I6WXVOUYuQj6+A0CdULNj1vBqH9+7uWbLZ6lrD4ba44x/wIDAQAB +AoGAZbWwowvCq1GBq4vPPRI3h739Uz0bRl1ymf1woYXNguXRtCB4yyH+2BTmmrrF +6AIWkePuUEdbUaKyK5nGu3iOWM+/i6NP3kopQANtbAYJ2ray3kwvFlhqyn1bxX4n +gl/Cbdw1If4zrDrB66y8mYDsjzK7n/gFaDNcY4GArjvOXKkCQQD9Lgv+WD73y4RP +yS+cRarlEeLLWVsX/pg2oEBLM50jsdUnrLSW071MjBgP37oOXzqynF9SoDbP2Y5C +x+aGux9LAkEA5qPlQPv0cv8Wc3qTI+LixZ/86PPHKWnOnwaHm3b9vQjZAkuVQg3n +Wgg9YDmPM87t3UFH7ZbDihUreUxwr9ZjnQJAZ9Z95shMsxbOYmbSVxafu6m1Sc+R +M+sghK7/D5jQpzYlhUspGf8n0YBX0hLhXUmjamQGGH5LXL4Owcb4/mM6twJAEVio +SF/qva9jv+GrKVrKFXT374lOJFY53Qn/rvifEtWUhLCslCA5kzLlctRBafMZPrfH +Mh5RrJP1BhVysDbenQJASGcc+DiF7rB6K++ZGyC11E2AP29DcZ0pgPESSV7npOGg ++NqPRZNVCSZOiVmNuejZqmwKhZNGZnBFx1Y+ChAAgw== +-----END RSA PRIVATE KEY-----`) + client2CertData = newCertificateData(`-----BEGIN CERTIFICATE----- +MIICBDCCAW2gAwIBAgIJAPgVBh+4xbGnMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAMMEHdlYmhvb2tfdGVzdHNfY2EwIBcNMTcwNzI4MjMxNTI4WhgPMjI5MTA1MTMy +MzE1MjhaMB8xHTAbBgNVBAMMFHdlYmhvb2tfdGVzdHNfY2xpZW50MIGfMA0GCSqG +SIb3DQEBAQUAA4GNADCBiQKBgQDQQLzbrmHbtlxE7wViaoXFp5tQx7zzM2Ed7O1E +gs3JUws5KkPbNrejLwixvLkzzU152M43UGsyKDn7HPyjXDogTZSW6C257XpYodk3 +S/gZS9oZtPss4UJuJioQk/M8X1ZjYP8kCTArOvVRJeNQL8GM7h5QQ6J5LUq+IdZb +T0retQIDAQABo0owSDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAU +BggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0B +AQsFAAOBgQBdAxoU5YAmp0d+5b4qg/xOGC5rKcnksQEXYoGwFBWwaKvh9oUlGGxI +A5Ykf2TEl24br4tLmicpdxUX4H4PbkdPxOjM9ghIKlmgHo8vBRC0iVIwYgQsw1W8 +ETY34Or+PJqaeslqx/t7kUKY5UIF9DLVolsIiAHveJNR2uBWiP0KiQ== +-----END CERTIFICATE-----`, `-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDQQLzbrmHbtlxE7wViaoXFp5tQx7zzM2Ed7O1Egs3JUws5KkPb +NrejLwixvLkzzU152M43UGsyKDn7HPyjXDogTZSW6C257XpYodk3S/gZS9oZtPss +4UJuJioQk/M8X1ZjYP8kCTArOvVRJeNQL8GM7h5QQ6J5LUq+IdZbT0retQIDAQAB +AoGBAMFjTL4IKvG4X+jXub1RxFXvNkkGos2Jaec7TH5xpZ4OUv7L4+We41tTYxSC +d83GGetLzPwK3vDd8DHkEiu1incket78rwmQ89LnQNyM0B5ejaTjW2zHcvKJ0Mtn +nM32juQfq8St9JZVweS87k8RkLt9cOrg6219MRbFO+1Vn8WhAkEA+/rqHCspBdXr +7RL+H63k7RjqBllVEYlw1ukqTw1gp5IImmeOwgl3aRrJJfFV6gxxEqQ4CCb2vf9M +yjrGEvP9KQJBANOTPcpskT/0dyipsAkvLFZTKjN+4fdfq37H3dVgMR6oQcMJwukd +cEio1Hx+XzXuD0RHXighq7bUzel+IqzRuq0CQBJkzpIf1G7InuA/cq19VCi6mNq9 +yqftEH+fpab/ov6YemhLBvDDICRcADL02wCqx9ZEhpKRxZE5AbIBeFQJ24ECQG4f +9cmnOPNRC7TengIpy6ojH5QuNu/LnDghUBYAO5D5g0FBk3JDIG6xceha3rPzdX7U +pu28mORRX9xpCyNpBwECQQCtDNZoehdPVuZA3Wocno31Rjmuy83ajgRRuEzqv0tj +uC6Jo2eLcSV1sSdzTjaaWdM6XeYj6yHOAm8ZBIQs7m6V +-----END RSA PRIVATE KEY-----`) +) + +type fakeManager struct { + cert atomic.Value // Always a *tls.Certificate +} + +func (f *fakeManager) SetCertificateSigningRequestClient(certificatesclient.CertificateSigningRequestInterface) error { + return nil +} + +func (f *fakeManager) Start() {} + +func (f *fakeManager) Current() *tls.Certificate { + if val := f.cert.Load(); val != nil { + return val.(*tls.Certificate) + } + return nil +} + +func (f *fakeManager) setCurrent(cert *tls.Certificate) { + f.cert.Store(cert) +} + +func TestRotateShutsDownConnections(t *testing.T) { + + // This test fails if you comment out the t.closeAllConns() call in + // transport.go and don't close connections on a rotate. + + stop := make(chan struct{}) + defer close(stop) + + m := new(fakeManager) + m.setCurrent(client1CertData.certificate) + + // The last certificate we've seen. + lastSeenLeafCert := new(atomic.Value) // Always *x509.Certificate + + lastSerialNumber := func() *big.Int { + if cert := lastSeenLeafCert.Load(); cert != nil { + return cert.(*x509.Certificate).SerialNumber + } + return big.NewInt(0) + } + + h := func(w http.ResponseWriter, r *http.Request) { + if r.TLS != nil && len(r.TLS.PeerCertificates) != 0 { + // Record the last TLS certificate the client sent. + lastSeenLeafCert.Store(r.TLS.PeerCertificates[0]) + } + w.Write([]byte(`{}`)) + } + + s := httptest.NewUnstartedServer(http.HandlerFunc(h)) + s.TLS = &tls.Config{ + // Just request a cert, we don't need to verify it. + ClientAuth: tls.RequestClientCert, + } + s.StartTLS() + defer s.Close() + + c := &rest.Config{ + Host: s.URL, + TLSClientConfig: rest.TLSClientConfig{ + // We don't care about the server's cert. + Insecure: true, + }, + ContentConfig: rest.ContentConfig{ + // This is a hack. We don't actually care about the serializer. + NegotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{}), + }, + } + + // Check for a new cert every 10 milliseconds + if err := updateTransport(stop, 10*time.Millisecond, c, m); err != nil { + t.Fatal(err) + } + + client, err := rest.UnversionedRESTClientFor(c) + if err != nil { + t.Fatal(err) + } + + if err := client.Get().Do().Error(); err != nil { + t.Fatal(err) + } + firstCertSerial := lastSerialNumber() + + // Change the manager's certificate. This should cause the client to shut down + // its connections to the server. + m.setCurrent(client2CertData.certificate) + + for i := 0; i < 5; i++ { + time.Sleep(time.Millisecond * 10) + client.Get().Do() + if firstCertSerial.Cmp(lastSerialNumber()) != 0 { + // The certificate changed! + return + } + } + + t.Errorf("certificate rotated but client never reconnected with new cert") +} From e7de2fde2f3af09594a0cc266775a7d83c50b29d Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 25 Sep 2017 23:28:37 -0400 Subject: [PATCH 02/11] UPSTREAM: 53037: Verify client cert before reusing existing bootstrap The cert may have expired. --- .../kubernetes/cmd/kubelet/app/bootstrap.go | 92 ++++++-- .../kubernetes/cmd/kubelet/app/server.go | 8 +- .../certificate/certificate_manager.go | 208 ++++++++++------- .../certificate/certificate_manager_test.go | 213 +++++++++++++++++- .../kubelet/certificate/certificate_store.go | 2 +- .../pkg/kubelet/certificate/kubelet.go | 6 +- .../pkg/kubelet/certificate/transport.go | 18 +- .../pkg/kubelet/certificate/transport_test.go | 7 +- .../kubernetes/pkg/kubelet/util/csr/csr.go | 61 ++++- .../pkg/kubelet/util/csr/csr_test.go | 4 +- .../k8s.io/client-go/tools/cache/listwatch.go | 10 +- 11 files changed, 497 insertions(+), 132 deletions(-) diff --git a/vendor/k8s.io/kubernetes/cmd/kubelet/app/bootstrap.go b/vendor/k8s.io/kubernetes/cmd/kubelet/app/bootstrap.go index 353a01f668d3..f8551d1cfb2a 100644 --- a/vendor/k8s.io/kubernetes/cmd/kubelet/app/bootstrap.go +++ b/vendor/k8s.io/kubernetes/cmd/kubelet/app/bootstrap.go @@ -18,18 +18,20 @@ package app import ( "fmt" - _ "net/http/pprof" "os" "path/filepath" + "time" "github.com/golang/glog" "k8s.io/apimachinery/pkg/types" - certificates "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/transport" certutil "k8s.io/client-go/util/cert" + certificatesclient "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/certificates/v1beta1" "k8s.io/kubernetes/pkg/kubelet/util/csr" ) @@ -38,22 +40,20 @@ const ( defaultKubeletClientKeyFile = "kubelet-client.key" ) -// bootstrapClientCert requests a client cert for kubelet if the kubeconfigPath file does not exist. +// BootstrapClientCert requests a client cert for kubelet if the kubeconfigPath file does not exist. // The kubeconfig at bootstrapPath is used to request a client certificate from the API server. // On success, a kubeconfig file referencing the generated key and obtained certificate is written to kubeconfigPath. // The certificate and key file are stored in certDir. func BootstrapClientCert(kubeconfigPath string, bootstrapPath string, certDir string, nodeName types.NodeName) error { - // Short-circuit if the kubeconfig file already exists. - // TODO: inspect the kubeconfig, ensure a rest client can be built from it, verify client cert expiration, etc. - _, err := os.Stat(kubeconfigPath) - if err == nil { - glog.V(2).Infof("Kubeconfig %s exists, skipping bootstrap", kubeconfigPath) - return nil - } - if !os.IsNotExist(err) { - glog.Errorf("Error reading kubeconfig %s, skipping bootstrap: %v", kubeconfigPath, err) + // Short-circuit if the kubeconfig file exists and is valid. + ok, err := verifyBootstrapClientConfig(kubeconfigPath) + if err != nil { return err } + if ok { + glog.V(2).Infof("Kubeconfig %s exists and is valid, skipping bootstrap", kubeconfigPath) + return nil + } glog.V(2).Info("Using bootstrap kubeconfig to generate TLS client cert, key and kubeconfig file") @@ -61,7 +61,7 @@ func BootstrapClientCert(kubeconfigPath string, bootstrapPath string, certDir st if err != nil { return fmt.Errorf("unable to load bootstrap kubeconfig: %v", err) } - bootstrapClient, err := certificates.NewForConfig(bootstrapClientConfig) + bootstrapClient, err := certificatesclient.NewForConfig(bootstrapClientConfig) if err != nil { return fmt.Errorf("unable to create certificates signing request client: %v", err) } @@ -73,6 +73,13 @@ func BootstrapClientCert(kubeconfigPath string, bootstrapPath string, certDir st if err != nil { return fmt.Errorf("unable to build bootstrap key path: %v", err) } + defer func() { + if !success { + if err := os.Remove(keyPath); err != nil && !os.IsNotExist(err) { + glog.Warningf("Cannot clean up the key file %q: %v", keyPath, err) + } + } + }() keyData, _, err := certutil.LoadOrGenerateKeyFile(keyPath) if err != nil { return err @@ -83,6 +90,13 @@ func BootstrapClientCert(kubeconfigPath string, bootstrapPath string, certDir st if err != nil { return fmt.Errorf("unable to build bootstrap client cert path: %v", err) } + defer func() { + if !success { + if err := os.Remove(certPath); err != nil && !os.IsNotExist(err) { + glog.Warningf("Cannot clean up the cert file %q: %v", certPath, err) + } + } + }() certData, err := csr.RequestNodeCertificate(bootstrapClient.CertificateSigningRequests(), keyData, nodeName) if err != nil { return err @@ -90,13 +104,6 @@ func BootstrapClientCert(kubeconfigPath string, bootstrapPath string, certDir st if err := certutil.WriteCert(certPath, certData); err != nil { return err } - defer func() { - if !success { - if err := os.Remove(certPath); err != nil { - glog.Warningf("Cannot clean up the cert file %q: %v", certPath, err) - } - } - }() // Get the CA data from the bootstrap client config. caFile, caData := bootstrapClientConfig.CAFile, []byte{} @@ -151,3 +158,48 @@ func loadRESTClientConfig(kubeconfig string) (*restclient.Config, error) { loader, ).ClientConfig() } + +// verifyBootstrapClientConfig checks the provided kubeconfig to see if it has a valid +// client certificate. It returns true if the kubeconfig is valid, or an error if bootstrapping +// should stop immediately. +func verifyBootstrapClientConfig(kubeconfigPath string) (bool, error) { + _, err := os.Stat(kubeconfigPath) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, fmt.Errorf("error reading existing bootstrap kubeconfig %s: %v", kubeconfigPath, err) + } + bootstrapClientConfig, err := loadRESTClientConfig(kubeconfigPath) + if err != nil { + utilruntime.HandleError(fmt.Errorf("Unable to read existing bootstrap client config: %v", err)) + return false, nil + } + transportConfig, err := bootstrapClientConfig.TransportConfig() + if err != nil { + utilruntime.HandleError(fmt.Errorf("Unable to load transport configuration from existing bootstrap client config: %v", err)) + return false, nil + } + // has side effect of populating transport config data fields + if _, err := transport.TLSConfigFor(transportConfig); err != nil { + utilruntime.HandleError(fmt.Errorf("Unable to load TLS configuration from existing bootstrap client config: %v", err)) + return false, nil + } + certs, err := certutil.ParseCertsPEM(transportConfig.TLS.CertData) + if err != nil { + utilruntime.HandleError(fmt.Errorf("Unable to load TLS certificates from existing bootstrap client config: %v", err)) + return false, nil + } + if len(certs) == 0 { + utilruntime.HandleError(fmt.Errorf("Unable to read TLS certificates from existing bootstrap client config: %v", err)) + return false, nil + } + now := time.Now() + for _, cert := range certs { + if now.After(cert.NotAfter) { + utilruntime.HandleError(fmt.Errorf("Part of the existing bootstrap client certificate is expired: %s", cert.NotAfter)) + return false, nil + } + } + return true, nil +} diff --git a/vendor/k8s.io/kubernetes/cmd/kubelet/app/server.go b/vendor/k8s.io/kubernetes/cmd/kubelet/app/server.go index d1d6cf4ec08d..73f6810746f0 100644 --- a/vendor/k8s.io/kubernetes/cmd/kubelet/app/server.go +++ b/vendor/k8s.io/kubernetes/cmd/kubelet/app/server.go @@ -24,13 +24,15 @@ import ( "math/rand" "net" "net/http" - _ "net/http/pprof" "net/url" "os" "path" "strconv" "time" + // included for side effect of getting pprof endpoints + _ "net/http/pprof" + "github.com/golang/glog" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -466,7 +468,9 @@ func run(s *options.KubeletServer, kubeDeps *kubelet.KubeletDeps) (err error) { if err != nil { return err } - if err := certificate.UpdateTransport(wait.NeverStop, clientConfig, clientCertificateManager); err != nil { + // we set exitIfExpired to true because we use this client configuration to request new certs - if we are unable + // to request new certs we will need to re-bootstrap + if err := certificate.UpdateTransport(wait.NeverStop, clientConfig, clientCertificateManager, true); err != nil { return err } } diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_manager.go b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_manager.go index c381010902b9..e3458eae46f2 100644 --- a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_manager.go +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_manager.go @@ -28,20 +28,27 @@ import ( "time" "github.com/golang/glog" + "github.com/prometheus/client_golang/prometheus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/api/errors" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/util/cert" certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" certificatesclient "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/certificates/v1beta1" + "k8s.io/kubernetes/pkg/kubelet/metrics" + "k8s.io/kubernetes/pkg/kubelet/util/csr" ) const ( - syncPeriod = 1 * time.Hour + certificateManagerSubsystem = "certificate_manager" + certificateExpirationKey = "expiration_seconds" ) +// certificateWaitBackoff controls the amount and timing of retries when the +// watch for certificate approval is interrupted. +var certificateWaitBackoff = wait.Backoff{Duration: 30 * time.Second, Steps: 4, Factor: 1.5, Jitter: 0.1} + // Manager maintains and updates the certificates in use by this certificate // manager. In the background it communicates with the API server to get new // certificates for certificates about to expire. @@ -55,10 +62,20 @@ type Manager interface { // certificate manager, as well as the associated certificate and key data // in PEM format. Current() *tls.Certificate + // ServerHealthy returns true if the manager is able to communicate with + // the server. This allows a caller to determine whether the cert manager + // thinks it can potentially talk to the API server. The cert manager may + // be very conservative and only return true if recent communication has + // occurred with the server. + ServerHealthy() bool } // Config is the set of configuration parameters available for a new Manager. type Config struct { + // Name is a name describing the certificate being managed by this + // certificate manager. It will be used for recording metrics relevant to + // the certificate. + Name string // CertificateSigningRequestClient will be used for signing new certificate // requests generated when a key rotation occurs. It must be set either at // initialization or by using CertificateSigningRequestClient before @@ -128,12 +145,18 @@ type manager struct { cert *tls.Certificate rotationDeadline time.Time forceRotation bool + certificateExpiration prometheus.Gauge + serverHealth bool } // NewManager returns a new certificate manager. A certificate manager is // responsible for being the authoritative source of certificates in the // Kubelet and handling updates due to rotation. func NewManager(config *Config) (Manager, error) { + if config.Name == "" { + return nil, fmt.Errorf("the 'Name' is required to disambiguate metric values of different certificate manager instances") + } + cert, forceRotation, err := getCurrentCertificateOrBootstrap( config.CertificateStore, config.BootstrapCertificatePEM, @@ -142,6 +165,17 @@ func NewManager(config *Config) (Manager, error) { return nil, err } + var certificateExpiration = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: metrics.KubeletSubsystem, + Subsystem: certificateManagerSubsystem, + Name: fmt.Sprintf("%s_%s", config.Name, certificateExpirationKey), + Help: "Gauge of the lifetime of a certificate. The value is the date the certificate will expire in seconds since January 1, 1970 UTC.", + }, + ) + + prometheus.MustRegister(certificateExpiration) + m := manager{ certSigningRequestClient: config.CertificateSigningRequestClient, template: config.Template, @@ -149,6 +183,7 @@ func NewManager(config *Config) (Manager, error) { certStore: config.CertificateStore, cert: cert, forceRotation: forceRotation, + certificateExpiration: certificateExpiration, } return &m, nil @@ -164,6 +199,14 @@ func (m *manager) Current() *tls.Certificate { return m.cert } +// ServerHealthy returns true if the cert manager believes the server +// is currently alive. +func (m *manager) ServerHealthy() bool { + m.certAccessLock.RLock() + defer m.certAccessLock.RUnlock() + return m.serverHealth +} + // SetCertificateSigningRequestClient sets the client interface that is used // for signing new certificates generated as part of rotation. It must be // called before Start() and can not be used to change the @@ -197,9 +240,9 @@ func (m *manager) Start() { // loop to allow bootstrap scenarios, where the certificate manager // doesn't have a certificate at all yet. if m.shouldRotate() { - _, err := m.rotateCerts() - if err != nil { - glog.Errorf("Could not rotate certificates: %v", err) + glog.V(1).Infof("shouldRotate() is true, forcing immediate rotation") + if _, err := m.rotateCerts(); err != nil { + utilruntime.HandleError(fmt.Errorf("Could not rotate certificates: %v", err)) } } backoff := wait.Backoff{ @@ -209,9 +252,11 @@ func (m *manager) Start() { Steps: 7, } go wait.Forever(func() { - time.Sleep(m.rotationDeadline.Sub(time.Now())) + sleepInterval := m.rotationDeadline.Sub(time.Now()) + glog.V(2).Infof("Waiting %v for next certificate rotation", sleepInterval) + time.Sleep(sleepInterval) if err := wait.ExponentialBackoff(backoff, m.rotateCerts); err != nil { - glog.Errorf("Reached backoff limit, still unable to rotate certs: %v", err) + utilruntime.HandleError(fmt.Errorf("Reached backoff limit, still unable to rotate certs: %v", err)) wait.PollInfinite(128*time.Second, m.rotateCerts) } }, 0) @@ -265,24 +310,58 @@ func (m *manager) shouldRotate() bool { return time.Now().After(m.rotationDeadline) } +// rotateCerts attempts to request a client cert from the server, wait a reasonable +// period of time for it to be signed, and then update the cert on disk. If it cannot +// retrieve a cert, it will return false. It will only return error in exceptional cases. +// This method also keeps track of "server health" by interpreting the responses it gets +// from the server on the various calls it makes. func (m *manager) rotateCerts() (bool, error) { - csrPEM, keyPEM, err := m.generateCSR() + glog.V(2).Infof("Rotating certificates") + + csrPEM, keyPEM, privateKey, err := m.generateCSR() if err != nil { - glog.Errorf("Unable to generate a certificate signing request: %v", err) + utilruntime.HandleError(fmt.Errorf("Unable to generate a certificate signing request: %v", err)) return false, nil } // Call the Certificate Signing Request API to get a certificate for the // new private key. - crtPEM, err := requestCertificate(m.certSigningRequestClient, csrPEM, m.usages) + req, err := csr.RequestCertificate(m.certSigningRequestClient, csrPEM, "", m.usages, privateKey) if err != nil { - glog.Errorf("Failed while requesting a signed certificate from the master: %v", err) + utilruntime.HandleError(fmt.Errorf("Failed while requesting a signed certificate from the master: %v", err)) + return false, m.updateServerError(err) + } + + // Wait for the certificate to be signed. Instead of one long watch, we retry with slighly longer + // intervals each time in order to tolerate failures from the server AND to preserve the liveliness + // of the cert manager loop. This creates slightly more traffic against the API server in return + // for bounding the amount of time we wait when a certificate expires. + var crtPEM []byte + watchDuration := time.Minute + if err := wait.ExponentialBackoff(certificateWaitBackoff, func() (bool, error) { + data, err := csr.WaitForCertificate(m.certSigningRequestClient, req, watchDuration) + switch { + case err == nil: + crtPEM = data + return true, nil + case err == wait.ErrWaitTimeout: + watchDuration += time.Minute + if watchDuration > 5*time.Minute { + watchDuration = 5 * time.Minute + } + return false, nil + default: + utilruntime.HandleError(fmt.Errorf("Unable to check certificate signing status: %v", err)) + return false, m.updateServerError(err) + } + }); err != nil { + utilruntime.HandleError(fmt.Errorf("Certificate request was not signed: %v", err)) return false, nil } cert, err := m.certStore.Update(crtPEM, keyPEM) if err != nil { - glog.Errorf("Unable to store the new cert/key pair: %v", err) + utilruntime.HandleError(fmt.Errorf("Unable to store the new cert/key pair: %v", err)) return false, nil } @@ -314,93 +393,58 @@ func (m *manager) setRotationDeadline() { jitteryDuration := wait.Jitter(time.Duration(totalDuration), 0.2) - time.Duration(totalDuration*0.3) m.rotationDeadline = m.cert.Leaf.NotBefore.Add(jitteryDuration) + glog.V(2).Infof("Certificate expiration is %v, rotation deadline is %v", notAfter, m.rotationDeadline) + m.certificateExpiration.Set(float64(notAfter.Unix())) } +// updateCached sets the most recent retrieved cert. It also sets the server +// as assumed healthy. func (m *manager) updateCached(cert *tls.Certificate) { m.certAccessLock.Lock() defer m.certAccessLock.Unlock() + m.serverHealth = true m.cert = cert } -func (m *manager) generateCSR() (csrPEM []byte, keyPEM []byte, err error) { +// updateServerError takes an error returned by the server and infers +// the health of the server based on the error. It will return nil if +// the error does not require immediate termination of any wait loops, +// and otherwise it will return the error. +func (m *manager) updateServerError(err error) error { + m.certAccessLock.Lock() + defer m.certAccessLock.Unlock() + switch { + case errors.IsUnauthorized(err): + // SSL terminating proxies may report this error instead of the master + m.serverHealth = true + case errors.IsUnexpectedServerError(err): + // generally indicates a proxy or other load balancer problem, rather than a problem coming + // from the master + m.serverHealth = false + default: + // Identify known errors that could be expected for a cert request that + // indicate everything is working normally + m.serverHealth = errors.IsNotFound(err) || errors.IsForbidden(err) + } + return nil +} + +func (m *manager) generateCSR() (csrPEM []byte, keyPEM []byte, key interface{}, err error) { // Generate a new private key. privateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader) if err != nil { - return nil, nil, fmt.Errorf("unable to generate a new private key: %v", err) + return nil, nil, nil, fmt.Errorf("unable to generate a new private key: %v", err) } der, err := x509.MarshalECPrivateKey(privateKey) if err != nil { - return nil, nil, fmt.Errorf("unable to marshal the new key to DER: %v", err) + return nil, nil, nil, fmt.Errorf("unable to marshal the new key to DER: %v", err) } keyPEM = pem.EncodeToMemory(&pem.Block{Type: cert.ECPrivateKeyBlockType, Bytes: der}) csrPEM, err = cert.MakeCSRFromTemplate(privateKey, m.template) if err != nil { - return nil, nil, fmt.Errorf("unable to create a csr from the private key: %v", err) - } - return csrPEM, keyPEM, nil -} - -// requestCertificate will create a certificate signing request using the PEM -// encoded CSR and send it to API server, then it will watch the object's -// status, once approved by API server, it will return the API server's issued -// certificate (pem-encoded). If there is any errors, or the watch timeouts, it -// will return an error. -// -// NOTE This is a copy of a function with the same name in -// k8s.io/kubernetes/pkg/kubelet/util/csr/csr.go, changing only the package that -// CertificateSigningRequestInterface and KeyUsage are imported from. -func requestCertificate(client certificatesclient.CertificateSigningRequestInterface, csrData []byte, usages []certificates.KeyUsage) (certData []byte, err error) { - glog.Infof("Requesting new certificate.") - req, err := client.Create(&certificates.CertificateSigningRequest{ - // Username, UID, Groups will be injected by API server. - TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"}, - ObjectMeta: metav1.ObjectMeta{GenerateName: "csr-"}, - - Spec: certificates.CertificateSigningRequestSpec{ - Request: csrData, - Usages: usages, - }, - }) - if err != nil { - return nil, fmt.Errorf("cannot create certificate signing request: %v", err) - } - - // Make a default timeout = 3600s. - var defaultTimeoutSeconds int64 = 3600 - certWatch, err := client.Watch(metav1.ListOptions{ - Watch: true, - TimeoutSeconds: &defaultTimeoutSeconds, - FieldSelector: fields.OneTermEqualSelector("metadata.name", req.Name).String(), - }) - if err != nil { - return nil, fmt.Errorf("cannot watch on the certificate signing request: %v", err) + return nil, nil, nil, fmt.Errorf("unable to create a csr from the private key: %v", err) } - defer certWatch.Stop() - ch := certWatch.ResultChan() - - for { - event, ok := <-ch - if !ok { - break - } - - if event.Type == watch.Modified || event.Type == watch.Added { - if event.Object.(*certificates.CertificateSigningRequest).UID != req.UID { - continue - } - status := event.Object.(*certificates.CertificateSigningRequest).Status - for _, c := range status.Conditions { - if c.Type == certificates.CertificateDenied { - return nil, fmt.Errorf("certificate signing request is not approved, reason: %v, message: %v", c.Reason, c.Message) - } - if c.Type == certificates.CertificateApproved && status.Certificate != nil { - return status.Certificate, nil - } - } - } - } - - return nil, fmt.Errorf("watch channel closed") + return csrPEM, keyPEM, privateKey, nil } diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_manager_test.go b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_manager_test.go index f6c5c913be59..4541dbc52959 100644 --- a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_manager_test.go +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_manager_test.go @@ -26,7 +26,12 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" + + "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" watch "k8s.io/apimachinery/pkg/watch" certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" certificatesclient "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/certificates/v1beta1" @@ -135,6 +140,7 @@ func TestNewManagerNoRotation(t *testing.T) { cert: storeCertData.certificate, } if _, err := NewManager(&Config{ + Name: "test_no_rotation", Template: &x509.CertificateRequest{}, Usages: []certificates.KeyUsage{}, CertificateStore: store, @@ -170,6 +176,11 @@ func TestShouldRotate(t *testing.T) { }, template: &x509.CertificateRequest{}, usages: []certificates.KeyUsage{}, + certificateExpiration: prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "test_gauge_name", + }, + ), } m.setRotationDeadline() if m.shouldRotate() != test.shouldRotate { @@ -212,6 +223,11 @@ func TestSetRotationDeadline(t *testing.T) { }, template: &x509.CertificateRequest{}, usages: []certificates.KeyUsage{}, + certificateExpiration: prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "test_gauge_name", + }, + ), } lowerBound := tc.notBefore.Add(time.Duration(float64(tc.notAfter.Sub(tc.notBefore)) * 0.7)) upperBound := tc.notBefore.Add(time.Duration(float64(tc.notAfter.Sub(tc.notBefore)) * 0.9)) @@ -270,6 +286,7 @@ func TestRotateCertWaitingForResultError(t *testing.T) { }, } + certificateWaitBackoff = wait.Backoff{Steps: 1} if success, err := m.rotateCerts(); success { t.Errorf("Got success from 'rotateCerts', wanted failure.") } else if err != nil { @@ -282,6 +299,7 @@ func TestNewManagerBootstrap(t *testing.T) { var cm Manager cm, err := NewManager(&Config{ + Name: "test_bootstrap", Template: &x509.CertificateRequest{}, Usages: []certificates.KeyUsage{}, CertificateStore: store, @@ -319,6 +337,7 @@ func TestNewManagerNoBootstrap(t *testing.T) { } cm, err := NewManager(&Config{ + Name: "test_no_bootstrap", Template: &x509.CertificateRequest{}, Usages: []certificates.KeyUsage{}, CertificateStore: store, @@ -454,13 +473,14 @@ func TestInitializeCertificateSigningRequestClient(t *testing.T) { }, } - for _, tc := range testCases { + for i, tc := range testCases { t.Run(tc.description, func(t *testing.T) { certificateStore := &fakeStore{ cert: tc.storeCert.certificate, } certificateManager, err := NewManager(&Config{ + Name: fmt.Sprintf("test_initialize_client_%d", i), Template: &x509.CertificateRequest{ Subject: pkix.Name{ Organization: []string{"system:nodes"}, @@ -555,13 +575,14 @@ func TestInitializeOtherRESTClients(t *testing.T) { }, } - for _, tc := range testCases { + for i, tc := range testCases { t.Run(tc.description, func(t *testing.T) { certificateStore := &fakeStore{ cert: tc.storeCert.certificate, } certificateManager, err := NewManager(&Config{ + Name: fmt.Sprintf("test_initialize_other_rest_clients_%d", i), Template: &x509.CertificateRequest{ Subject: pkix.Name{ Organization: []string{"system:nodes"}, @@ -594,10 +615,14 @@ func TestInitializeOtherRESTClients(t *testing.T) { } else { m.setRotationDeadline() if m.shouldRotate() { - if success, err := certificateManager.(*manager).rotateCerts(); !success { - t.Errorf("Got failure from 'rotateCerts', expected success") - } else if err != nil { + success, err := certificateManager.(*manager).rotateCerts() + if err != nil { t.Errorf("Got error %v, expected none.", err) + return + } + if !success { + t.Errorf("Unexpected response 'rotateCerts': %t", success) + return } } } @@ -610,6 +635,162 @@ func TestInitializeOtherRESTClients(t *testing.T) { } } +func TestServerHealth(t *testing.T) { + type certs struct { + storeCert *certificateData + bootstrapCert *certificateData + apiCert *certificateData + expectedCertBeforeStart *certificateData + expectedCertAfterStart *certificateData + } + + updatedCerts := certs{ + storeCert: storeCertData, + bootstrapCert: bootstrapCertData, + apiCert: apiServerCertData, + expectedCertBeforeStart: storeCertData, + expectedCertAfterStart: apiServerCertData, + } + + currentCerts := certs{ + storeCert: storeCertData, + bootstrapCert: bootstrapCertData, + apiCert: apiServerCertData, + expectedCertBeforeStart: storeCertData, + expectedCertAfterStart: storeCertData, + } + + testCases := []struct { + description string + certs + + failureType fakeClientFailureType + clientErr error + + expectRotateFail bool + expectHealthy bool + }{ + { + description: "Current certificate, bootstrap certificate", + certs: updatedCerts, + expectHealthy: true, + }, + { + description: "Generic error on create", + certs: currentCerts, + + failureType: createError, + expectRotateFail: true, + }, + { + description: "Unauthorized error on create", + certs: currentCerts, + + failureType: createError, + clientErr: errors.NewUnauthorized("unauthorized"), + expectRotateFail: true, + expectHealthy: true, + }, + { + description: "Generic unauthorized error on create", + certs: currentCerts, + + failureType: createError, + clientErr: errors.NewGenericServerResponse(401, "POST", schema.GroupResource{}, "", "", 0, true), + expectRotateFail: true, + expectHealthy: true, + }, + { + description: "Generic not found error on create", + certs: currentCerts, + + failureType: createError, + clientErr: errors.NewGenericServerResponse(404, "POST", schema.GroupResource{}, "", "", 0, true), + expectRotateFail: true, + expectHealthy: false, + }, + { + description: "Not found error on create", + certs: currentCerts, + + failureType: createError, + clientErr: errors.NewGenericServerResponse(404, "POST", schema.GroupResource{}, "", "", 0, false), + expectRotateFail: true, + expectHealthy: true, + }, + { + description: "Conflict error on watch", + certs: currentCerts, + + failureType: watchError, + clientErr: errors.NewGenericServerResponse(409, "POST", schema.GroupResource{}, "", "", 0, false), + expectRotateFail: true, + expectHealthy: false, + }, + } + + for i, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + certificateStore := &fakeStore{ + cert: tc.storeCert.certificate, + } + + certificateManager, err := NewManager(&Config{ + Name: fmt.Sprintf("test_server_health_%d", i), + Template: &x509.CertificateRequest{ + Subject: pkix.Name{ + Organization: []string{"system:nodes"}, + CommonName: "system:node:fake-node-name", + }, + }, + Usages: []certificates.KeyUsage{ + certificates.UsageDigitalSignature, + certificates.UsageKeyEncipherment, + certificates.UsageClientAuth, + }, + CertificateStore: certificateStore, + BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, + BootstrapKeyPEM: tc.bootstrapCert.keyPEM, + CertificateSigningRequestClient: &fakeClient{ + certificatePEM: tc.apiCert.certificatePEM, + failureType: tc.failureType, + err: tc.clientErr, + }, + }) + if err != nil { + t.Errorf("Got %v, wanted no error.", err) + } + + certificate := certificateManager.Current() + if !certificatesEqual(certificate, tc.expectedCertBeforeStart.certificate) { + t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertBeforeStart.certificate)) + } + + if _, ok := certificateManager.(*manager); !ok { + t.Errorf("Expected a '*manager' from 'NewManager'") + } else { + success, err := certificateManager.(*manager).rotateCerts() + if err != nil { + t.Errorf("Got error %v, expected none.", err) + return + } + if !success != tc.expectRotateFail { + t.Errorf("Unexpected response 'rotateCerts': %t", success) + return + } + if actual := certificateManager.(*manager).ServerHealthy(); actual != tc.expectHealthy { + t.Errorf("Unexpected manager server health: %t", actual) + } + } + + certificate = certificateManager.Current() + if !certificatesEqual(certificate, tc.expectedCertAfterStart.certificate) { + t.Errorf("Got %v, wanted %v", certificateString(certificate), certificateString(tc.expectedCertAfterStart.certificate)) + } + }) + } +} + type fakeClientFailureType int const ( @@ -623,10 +804,29 @@ type fakeClient struct { certificatesclient.CertificateSigningRequestInterface failureType fakeClientFailureType certificatePEM []byte + err error +} + +func (c fakeClient) List(opts v1.ListOptions) (*certificates.CertificateSigningRequestList, error) { + if c.failureType == watchError { + if c.err != nil { + return nil, c.err + } + return nil, fmt.Errorf("Watch error") + } + csrReply := certificates.CertificateSigningRequestList{ + Items: []certificates.CertificateSigningRequest{ + {ObjectMeta: v1.ObjectMeta{UID: "fake-uid"}}, + }, + } + return &csrReply, nil } func (c fakeClient) Create(*certificates.CertificateSigningRequest) (*certificates.CertificateSigningRequest, error) { if c.failureType == createError { + if c.err != nil { + return nil, c.err + } return nil, fmt.Errorf("Create error") } csrReply := certificates.CertificateSigningRequest{} @@ -636,6 +836,9 @@ func (c fakeClient) Create(*certificates.CertificateSigningRequest) (*certificat func (c fakeClient) Watch(opts v1.ListOptions) (watch.Interface, error) { if c.failureType == watchError { + if c.err != nil { + return nil, c.err + } return nil, fmt.Errorf("Watch error") } return &fakeWatch{ diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_store.go b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_store.go index 53f4d98a4a4f..49c084b7758e 100644 --- a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_store.go +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/certificate_store.go @@ -184,7 +184,7 @@ func loadCertKeyBlocks(pairFile string) (cert *pem.Block, key *pem.Block, err er if certBlock == nil { return nil, nil, fmt.Errorf("could not decode the first block from %q from expected PEM format", pairFile) } - keyBlock, rest := pem.Decode(rest) + keyBlock, _ := pem.Decode(rest) if keyBlock == nil { return nil, nil, fmt.Errorf("could not decode the second block from %q from expected PEM format", pairFile) } diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/kubelet.go b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/kubelet.go index ad91f63c8424..1a54bd5ef598 100644 --- a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/kubelet.go +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/kubelet.go @@ -46,6 +46,7 @@ func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg return nil, fmt.Errorf("failed to initialize server certificate store: %v", err) } m, err := NewManager(&Config{ + Name: "server", CertificateSigningRequestClient: certSigningRequestClient, Template: &x509.CertificateRequest{ Subject: pkix.Name{ @@ -62,7 +63,7 @@ func NewKubeletServerCertificateManager(kubeClient clientset.Interface, kubeCfg // digital signatures used during TLS negotiation. certificates.UsageDigitalSignature, // KeyEncipherment allows the cert/key pair to be used to encrypt - // keys, including the symetric keys negotiated during TLS setup + // keys, including the symmetric keys negotiated during TLS setup // and used for data transfer. certificates.UsageKeyEncipherment, // ServerAuth allows the cert to be used by a TLS server to @@ -92,6 +93,7 @@ func NewKubeletClientCertificateManager(certDirectory string, nodeName types.Nod return nil, fmt.Errorf("failed to initialize client certificate store: %v", err) } m, err := NewManager(&Config{ + Name: "client", Template: &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: fmt.Sprintf("system:node:%s", nodeName), @@ -106,7 +108,7 @@ func NewKubeletClientCertificateManager(certDirectory string, nodeName types.Nod // negotiation. certificates.UsageDigitalSignature, // KeyEncipherment allows the cert/key pair to be used to encrypt - // keys, including the symetric keys negotiated during TLS setup + // keys, including the symmetric keys negotiated during TLS setup // and used for data transfer.. certificates.UsageKeyEncipherment, // ClientAuth allows the cert to be used by a TLS client to diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport.go b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport.go index bb472039f3fb..0461c82943db 100644 --- a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport.go +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport.go @@ -44,13 +44,18 @@ import ( // // stopCh should be used to indicate when the transport is unused and doesn't need // to continue checking the manager. -func UpdateTransport(stopCh <-chan struct{}, clientConfig *restclient.Config, clientCertificateManager Manager) error { - return updateTransport(stopCh, 10*time.Second, clientConfig, clientCertificateManager) +// +// exitIfExpired controls whether the transport triggers a fatal error if the +// currently returned certificate is expired. This can be used by the caller to +// ensure that if the manager falls behind and is unable to rotate a certificate +// that the process exits. +func UpdateTransport(stopCh <-chan struct{}, clientConfig *restclient.Config, clientCertificateManager Manager, exitIfExpired bool) error { + return updateTransport(stopCh, 10*time.Second, clientConfig, clientCertificateManager, exitIfExpired) } // updateTransport is an internal method that exposes how often this method checks that the // client cert has changed. Intended for testing. -func updateTransport(stopCh <-chan struct{}, period time.Duration, clientConfig *restclient.Config, clientCertificateManager Manager) error { +func updateTransport(stopCh <-chan struct{}, period time.Duration, clientConfig *restclient.Config, clientCertificateManager Manager, exitIfExpired bool) error { if clientConfig.Transport != nil { return fmt.Errorf("there is already a transport configured") } @@ -79,6 +84,13 @@ func updateTransport(stopCh <-chan struct{}, period time.Duration, clientConfig lastCert := clientCertificateManager.Current() go wait.Until(func() { curr := clientCertificateManager.Current() + if exitIfExpired && curr != nil && time.Now().After(curr.Leaf.NotAfter) { + if clientCertificateManager.ServerHealthy() { + glog.Fatalf("The currently active client certificate has expired and the server is responsive, exiting.") + } else { + glog.Errorf("The currently active client certificate has expired, but the server is not responsive. A restart may be necessary to retrieve new initial credentials.") + } + } if curr == nil || lastCert == curr { // Cert hasn't been rotated. return diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport_test.go b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport_test.go index 1067f79c9d93..7f6750479691 100644 --- a/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport_test.go +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/certificate/transport_test.go @@ -90,13 +90,16 @@ uC6Jo2eLcSV1sSdzTjaaWdM6XeYj6yHOAm8ZBIQs7m6V ) type fakeManager struct { - cert atomic.Value // Always a *tls.Certificate + cert atomic.Value // Always a *tls.Certificate + healthy bool } func (f *fakeManager) SetCertificateSigningRequestClient(certificatesclient.CertificateSigningRequestInterface) error { return nil } +func (f *fakeManager) ServerHealthy() bool { return f.healthy } + func (f *fakeManager) Start() {} func (f *fakeManager) Current() *tls.Certificate { @@ -160,7 +163,7 @@ func TestRotateShutsDownConnections(t *testing.T) { } // Check for a new cert every 10 milliseconds - if err := updateTransport(stop, 10*time.Millisecond, c, m); err != nil { + if err := updateTransport(stop, 10*time.Millisecond, c, m, false); err != nil { t.Fatal(err) } diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/util/csr/csr.go b/vendor/k8s.io/kubernetes/pkg/kubelet/util/csr/csr.go index 49e68af0e42f..ff57197b2949 100644 --- a/vendor/k8s.io/kubernetes/pkg/kubelet/util/csr/csr.go +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/util/csr/csr.go @@ -32,11 +32,12 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" - certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" - certificates "k8s.io/client-go/pkg/apis/certificates/v1beta1" "k8s.io/client-go/tools/cache" certutil "k8s.io/client-go/util/cert" + certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" + certificatesclient "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/certificates/v1beta1" ) // RequestNodeCertificate will create a certificate signing request for a node @@ -67,16 +68,20 @@ func RequestNodeCertificate(client certificatesclient.CertificateSigningRequestI certificates.UsageClientAuth, } name := digestedName(privateKeyData, subject, usages) - return requestCertificate(client, csrData, name, usages, privateKey) + req, err := RequestCertificate(client, csrData, name, usages, privateKey) + if err != nil { + return nil, err + } + return WaitForCertificate(client, req, 3600*time.Second) } -// requestCertificate will either use an existing (if this process has run +// RequestCertificate will either use an existing (if this process has run // before but not to completion) or create a certificate signing request using the // PEM encoded CSR and send it to API server, then it will watch the object's // status, once approved by API server, it will return the API server's issued // certificate (pem-encoded). If there is any errors, or the watch timeouts, it // will return an error. -func requestCertificate(client certificatesclient.CertificateSigningRequestInterface, csrData []byte, name string, usages []certificates.KeyUsage, privateKey interface{}) (certData []byte, err error) { +func RequestCertificate(client certificatesclient.CertificateSigningRequestInterface, csrData []byte, name string, usages []certificates.KeyUsage, privateKey interface{}) (req *certificates.CertificateSigningRequest, err error) { csr := &certificates.CertificateSigningRequest{ // Username, UID, Groups will be injected by API server. TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"}, @@ -88,28 +93,35 @@ func requestCertificate(client certificatesclient.CertificateSigningRequestInter Usages: usages, }, } + if len(csr.Name) == 0 { + csr.GenerateName = "csr-" + } - req, err := client.Create(csr) + req, err = client.Create(csr) switch { case err == nil: - case errors.IsAlreadyExists(err): + case errors.IsAlreadyExists(err) && len(name) > 0: glog.Infof("csr for this node already exists, reusing") req, err = client.Get(name, metav1.GetOptions{}) if err != nil { - return nil, fmt.Errorf("cannot retrieve certificate signing request: %v", err) + return nil, formatError("cannot retrieve certificate signing request: %v", err) } if err := ensureCompatible(req, csr, privateKey); err != nil { return nil, fmt.Errorf("retrieved csr is not compatible: %v", err) } glog.Infof("csr for this node is still valid") default: - return nil, fmt.Errorf("cannot create certificate signing request: %v", err) + return nil, formatError("cannot create certificate signing request: %v", err) } + return req, nil +} +// WaitForCertificate waits for a certificate to be issued until timeout, or returns an error. +func WaitForCertificate(client certificatesclient.CertificateSigningRequestInterface, req *certificates.CertificateSigningRequest, timeout time.Duration) (certData []byte, err error) { fieldSelector := fields.OneTermEqualSelector("metadata.name", req.Name).String() event, err := cache.ListWatchUntil( - 3600*time.Second, + timeout, &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { options.FieldSelector = fieldSelector @@ -123,6 +135,8 @@ func requestCertificate(client certificatesclient.CertificateSigningRequestInter func(event watch.Event) (bool, error) { switch event.Type { case watch.Modified, watch.Added: + case watch.Deleted: + return false, fmt.Errorf("csr %q was deleted", req.Name) default: return false, nil } @@ -141,12 +155,14 @@ func requestCertificate(client certificatesclient.CertificateSigningRequestInter return false, nil }, ) + if err == wait.ErrWaitTimeout { + return nil, wait.ErrWaitTimeout + } if err != nil { - return nil, fmt.Errorf("cannot watch on the certificate signing request: %v", err) + return nil, formatError("unable to retrieve certificate: %v", err) } return event.Object.(*certificates.CertificateSigningRequest).Status.Certificate, nil - } // This digest should include all the relevant pieces of the CSR we care about. @@ -202,5 +218,26 @@ func ensureCompatible(new, orig *certificates.CertificateSigningRequest, private if err := newCsr.CheckSignature(); err != nil { return fmt.Errorf("error validating signature new CSR against old key: %v", err) } + if len(new.Status.Certificate) > 0 { + certs, err := certutil.ParseCertsPEM(new.Status.Certificate) + if err != nil { + return fmt.Errorf("error parsing signed certificate for CSR: %v", err) + } + now := time.Now() + for _, cert := range certs { + if now.After(cert.NotAfter) { + return fmt.Errorf("one of the certificates for the CSR has expired: %s", cert.NotAfter) + } + } + } return nil } + +func formatError(format string, err error) error { + if s, ok := err.(errors.APIStatus); ok { + se := &errors.StatusError{ErrStatus: s.Status()} + se.ErrStatus.Message = fmt.Sprintf(format, se.ErrStatus.Message) + return se + } + return fmt.Errorf(format, err) +} diff --git a/vendor/k8s.io/kubernetes/pkg/kubelet/util/csr/csr_test.go b/vendor/k8s.io/kubernetes/pkg/kubelet/util/csr/csr_test.go index 1bbb286ef85b..d3e7423f5292 100644 --- a/vendor/k8s.io/kubernetes/pkg/kubelet/util/csr/csr_test.go +++ b/vendor/k8s.io/kubernetes/pkg/kubelet/util/csr/csr_test.go @@ -23,9 +23,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" watch "k8s.io/apimachinery/pkg/watch" - certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" - certificates "k8s.io/client-go/pkg/apis/certificates/v1beta1" certutil "k8s.io/client-go/util/cert" + certificates "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" + certificatesclient "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/certificates/v1beta1" ) func TestRequestNodeCertificateNoKeyData(t *testing.T) { diff --git a/vendor/k8s.io/kubernetes/staging/src/k8s.io/client-go/tools/cache/listwatch.go b/vendor/k8s.io/kubernetes/staging/src/k8s.io/client-go/tools/cache/listwatch.go index af01d4745798..98829835df90 100644 --- a/vendor/k8s.io/kubernetes/staging/src/k8s.io/client-go/tools/cache/listwatch.go +++ b/vendor/k8s.io/kubernetes/staging/src/k8s.io/client-go/tools/cache/listwatch.go @@ -23,6 +23,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" restclient "k8s.io/client-go/rest" ) @@ -95,6 +96,8 @@ func (lw *ListWatch) Watch(options metav1.ListOptions) (watch.Interface, error) return lw.WatchFunc(options) } +// ListWatchUntil checks the provided conditions against the items returned by the list watcher, returning wait.ErrWaitTimeout +// if timeout is exceeded without all conditions returning true, or an error if an error occurs. // TODO: check for watch expired error and retry watch from latest point? Same issue exists for Until. func ListWatchUntil(timeout time.Duration, lw ListerWatcher, conditions ...watch.ConditionFunc) (*watch.Event, error) { if len(conditions) == 0 { @@ -158,5 +161,10 @@ func ListWatchUntil(timeout time.Duration, lw ListerWatcher, conditions ...watch return nil, err } - return watch.Until(timeout, watchInterface, remainingConditions...) + evt, err := watch.Until(timeout, watchInterface, remainingConditions...) + if err == watch.ErrWatchClosed { + // present a consistent error interface to callers + err = wait.ErrWaitTimeout + } + return evt, err } From 79750e98f9fb161f5ffd4504c74e93b9b5aa6c67 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sun, 1 Oct 2017 18:26:55 -0400 Subject: [PATCH 03/11] Allow resync in oc observe without --names --- pkg/oc/cli/cmd/observe/observe.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/oc/cli/cmd/observe/observe.go b/pkg/oc/cli/cmd/observe/observe.go index 4a7859666b32..9377a1cc9387 100644 --- a/pkg/oc/cli/cmd/observe/observe.go +++ b/pkg/oc/cli/cmd/observe/observe.go @@ -212,7 +212,7 @@ func NewCmdObserve(fullName string, f *clientcmd.Factory, out, errOut io.Writer) // control observe program behavior cmd.Flags().BoolVar(&options.once, "once", false, "If true, exit with a status code 0 after all current objects have been processed.") cmd.Flags().DurationVar(&options.exitAfterPeriod, "exit-after", 0, "Exit with status code 0 after the provided duration, optional.") - cmd.Flags().DurationVar(&options.resyncPeriod, "resync-period", 0, "When non-zero, periodically reprocess every item from the server as a Sync event. Use to ensure external systems are kept up to date. Requires --names") + cmd.Flags().DurationVar(&options.resyncPeriod, "resync-period", 0, "When non-zero, periodically reprocess every item from the server as a Sync event. Use to ensure external systems are kept up to date.") cmd.Flags().BoolVar(&options.printMetricsOnExit, "print-metrics-on-exit", false, "If true, on exit write all metrics to stdout.") cmd.Flags().StringVar(&options.listenAddr, "listen-addr", options.listenAddr, "The name of an interface to listen on to expose metrics and health checking.") @@ -378,7 +378,7 @@ func (o *ObserveOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args return outputNames, nil } o.knownObjects = o.argumentStore - case len(o.deleteCommand) > 0: + case len(o.deleteCommand) > 0, o.resyncPeriod > 0: o.knownObjects = o.argumentStore } From 1b2f9998b9a2f203bd7a8f29ebcce786358c66a3 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sat, 30 Sep 2017 18:59:59 -0400 Subject: [PATCH 04/11] Tolerate being unable to remove /var/run/openshift-sdn When we mount /var/run/openshift-sdn into the container, we need to be able to clear its contents but the directory itself cannot be removed as it is a mount point. Also clarify one error. --- pkg/network/node/cniserver/cniserver.go | 7 +++++-- pkg/network/node/node.go | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/network/node/cniserver/cniserver.go b/pkg/network/node/cniserver/cniserver.go index 3fbd977cd164..86797a97ad52 100644 --- a/pkg/network/node/cniserver/cniserver.go +++ b/pkg/network/node/cniserver/cniserver.go @@ -126,8 +126,11 @@ func (s *CNIServer) Start(requestFunc cniRequestFunc) error { // Remove and re-create the socket directory with root-only permissions dirName := path.Dir(s.path) - if err := os.RemoveAll(dirName); err != nil { - return fmt.Errorf("failed to removing old pod info socket: %v", err) + if err := os.RemoveAll(s.path); err != nil && !os.IsNotExist(err) { + utilruntime.HandleError(fmt.Errorf("failed to remove old pod info socket: %v", err)) + } + if err := os.RemoveAll(dirName); err != nil && !os.IsNotExist(err) { + utilruntime.HandleError(fmt.Errorf("failed to remove contents of socket directory: %v", err)) } if err := os.MkdirAll(dirName, 0700); err != nil { return fmt.Errorf("failed to create pod info socket directory: %v", err) diff --git a/pkg/network/node/node.go b/pkg/network/node/node.go index 3ecc3c523d5c..dcbb71097ce6 100644 --- a/pkg/network/node/node.go +++ b/pkg/network/node/node.go @@ -345,7 +345,7 @@ func (node *OsdnNode) Start() error { networkChanged, err := node.SetupSDN() if err != nil { - return err + return fmt.Errorf("node SDN setup failed: %v", err) } err = node.SubnetStartNode() From ae01595b5bce65870aaaeddb8393126dc39ab754 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sat, 30 Sep 2017 17:26:06 -0400 Subject: [PATCH 05/11] The proxy health server should be on, it does not leak info This makes running in a separate process for networks able to have a health check and for metrics to be reported. --- pkg/cmd/server/kubernetes/network/network.go | 34 ++++++++++++++++--- .../server/kubernetes/node/options/options.go | 8 +++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/server/kubernetes/network/network.go b/pkg/cmd/server/kubernetes/network/network.go index be33c719daaf..66f70f7b4a30 100644 --- a/pkg/cmd/server/kubernetes/network/network.go +++ b/pkg/cmd/server/kubernetes/network/network.go @@ -1,12 +1,17 @@ package network import ( + "fmt" "net" + "net/http" + "time" "github.com/golang/glog" + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilnet "k8s.io/apimachinery/pkg/util/net" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilwait "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes/scheme" kv1core "k8s.io/client-go/kubernetes/typed/core/v1" @@ -73,6 +78,10 @@ func (c *NetworkConfig) RunProxy() { var proxier proxy.ProxyProvider var servicesHandler pconfig.ServiceHandler var endpointsHandler pconfig.EndpointsHandler + var healthzServer *healthcheck.HealthzServer + if len(c.ProxyConfig.HealthzBindAddress) > 0 { + healthzServer = healthcheck.NewDefaultHealthzServer(c.ProxyConfig.HealthzBindAddress, 2*c.ProxyConfig.IPTables.SyncPeriod.Duration) + } switch c.ProxyConfig.Mode { case componentconfig.ProxyModeIPTables: @@ -80,10 +89,6 @@ func (c *NetworkConfig) RunProxy() { if bindAddr.Equal(net.IPv4zero) { bindAddr = getNodeIP(c.ExternalKubeClientset.CoreV1(), hostname) } - var healthzServer *healthcheck.HealthzServer - if len(c.ProxyConfig.HealthzBindAddress) > 0 { - healthzServer = healthcheck.NewDefaultHealthzServer(c.ProxyConfig.HealthzBindAddress, 2*c.ProxyConfig.IPTables.SyncPeriod.Duration) - } if c.ProxyConfig.IPTables.MasqueradeBit == nil { // IPTablesMasqueradeBit must be specified or defaulted. glog.Fatalf("Unable to read IPTablesMasqueradeBit from config") @@ -102,6 +107,7 @@ func (c *NetworkConfig) RunProxy() { recorder, healthzServer, ) + iptables.RegisterMetrics() if err != nil { glog.Fatalf("error: Could not initialize Kubernetes Proxy. You must run this process as root (and if containerized, in the host network namespace as privileged) to use the service proxy: %v", err) @@ -195,6 +201,26 @@ func (c *NetworkConfig) RunProxy() { endpointsConfig.RegisterEventHandler(endpointsHandler) go endpointsConfig.Run(utilwait.NeverStop) + // Start up healthz server + if len(c.ProxyConfig.HealthzBindAddress) > 0 { + healthzServer.Run() + } + + // Start up a metrics server if requested + if len(c.ProxyConfig.MetricsBindAddress) > 0 { + mux := http.NewServeMux() + mux.HandleFunc("/proxyMode", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "%s", c.ProxyConfig.Mode) + }) + mux.Handle("/metrics", prometheus.Handler()) + go utilwait.Until(func() { + err := http.ListenAndServe(c.ProxyConfig.MetricsBindAddress, mux) + if err != nil { + utilruntime.HandleError(fmt.Errorf("starting metrics server failed: %v", err)) + } + }, 5*time.Second, utilwait.NeverStop) + } + // periodically sync k8s iptables rules go utilwait.Forever(proxier.SyncLoop, 0) glog.Infof("Started Kubernetes Proxy on %s", c.ProxyConfig.BindAddress) diff --git a/pkg/cmd/server/kubernetes/node/options/options.go b/pkg/cmd/server/kubernetes/node/options/options.go index e1a72655c538..5039e557b99b 100644 --- a/pkg/cmd/server/kubernetes/node/options/options.go +++ b/pkg/cmd/server/kubernetes/node/options/options.go @@ -158,9 +158,7 @@ func buildKubeProxyConfig(options configapi.NodeConfig) (*componentconfig.KubePr return nil, fmt.Errorf("The provided value to bind to must be an ip:port: %q", addr) } proxyconfig.BindAddress = ip.String() - - // HealthzPort, HealthzBindAddress - disable - proxyconfig.HealthzBindAddress = "" + // MetricsBindAddress - disable proxyconfig.MetricsBindAddress = "" // OOMScoreAdj, ResourceContainer - clear, we don't run in a container @@ -199,6 +197,10 @@ func buildKubeProxyConfig(options configapi.NodeConfig) (*componentconfig.KubePr return nil, kerrors.NewAggregate(err) } + if err := proxyOptions.Complete(); err != nil { + return nil, err + } + return proxyconfig, nil } From 13ef9b9e0167765b72bc4b925302abe1164e75e1 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sat, 30 Sep 2017 17:23:07 -0400 Subject: [PATCH 06/11] Allow network to cross compile on master --- .../{network_linux.go => network_sdn.go} | 0 .../origin/controller/network_unsupported.go | 22 ------------------- pkg/network/common/common.go | 2 -- pkg/network/common/common_test.go | 2 -- pkg/network/common/dns.go | 2 -- pkg/network/common/dns_test.go | 2 -- pkg/network/common/egress_dns.go | 2 -- pkg/network/common/eventqueue.go | 2 -- pkg/network/common/eventqueue_test.go | 2 -- pkg/network/master/master.go | 9 ++++---- pkg/network/master/master_test.go | 2 -- pkg/network/master/netid/allocator.go | 2 -- pkg/network/master/netid/allocator_test.go | 2 -- pkg/network/master/netid/netid.go | 2 -- pkg/network/master/netid/netid_test.go | 2 -- pkg/network/master/subnets.go | 2 -- pkg/network/master/vnids.go | 2 -- pkg/network/master/vnids_test.go | 2 -- 18 files changed, 5 insertions(+), 56 deletions(-) rename pkg/cmd/server/origin/controller/{network_linux.go => network_sdn.go} (100%) delete mode 100644 pkg/cmd/server/origin/controller/network_unsupported.go diff --git a/pkg/cmd/server/origin/controller/network_linux.go b/pkg/cmd/server/origin/controller/network_sdn.go similarity index 100% rename from pkg/cmd/server/origin/controller/network_linux.go rename to pkg/cmd/server/origin/controller/network_sdn.go diff --git a/pkg/cmd/server/origin/controller/network_unsupported.go b/pkg/cmd/server/origin/controller/network_unsupported.go deleted file mode 100644 index 0becb30d1549..000000000000 --- a/pkg/cmd/server/origin/controller/network_unsupported.go +++ /dev/null @@ -1,22 +0,0 @@ -// +build !linux - -package controller - -import ( - "fmt" - - configapi "github.com/openshift/origin/pkg/cmd/server/api" - "github.com/openshift/origin/pkg/network" -) - -type SDNControllerConfig struct { - NetworkConfig configapi.MasterNetworkConfig -} - -func (c *SDNControllerConfig) RunController(ctx ControllerContext) (bool, error) { - if !network.IsOpenShiftNetworkPlugin(c.NetworkConfig.NetworkPluginName) { - return false, nil - } - - return false, fmt.Errorf("SDN not supported on this platform") -} diff --git a/pkg/network/common/common.go b/pkg/network/common/common.go index d0cb5bde5736..b141b972b3bc 100644 --- a/pkg/network/common/common.go +++ b/pkg/network/common/common.go @@ -1,5 +1,3 @@ -// +build linux - package common import ( diff --git a/pkg/network/common/common_test.go b/pkg/network/common/common_test.go index 17e27393a3b7..7da08f5afa4a 100644 --- a/pkg/network/common/common_test.go +++ b/pkg/network/common/common_test.go @@ -1,5 +1,3 @@ -// +build linux - package common import ( diff --git a/pkg/network/common/dns.go b/pkg/network/common/dns.go index d40f7dc85504..28170704cb99 100644 --- a/pkg/network/common/dns.go +++ b/pkg/network/common/dns.go @@ -1,5 +1,3 @@ -// +build linux - package common import ( diff --git a/pkg/network/common/dns_test.go b/pkg/network/common/dns_test.go index 7e52f583c525..a5b5c23e44e7 100644 --- a/pkg/network/common/dns_test.go +++ b/pkg/network/common/dns_test.go @@ -1,5 +1,3 @@ -// +build linux - package common import ( diff --git a/pkg/network/common/egress_dns.go b/pkg/network/common/egress_dns.go index 2eee7c80305f..1dee42793d3a 100644 --- a/pkg/network/common/egress_dns.go +++ b/pkg/network/common/egress_dns.go @@ -1,5 +1,3 @@ -// +build linux - package common import ( diff --git a/pkg/network/common/eventqueue.go b/pkg/network/common/eventqueue.go index f054f7a094e0..b08e79184c93 100644 --- a/pkg/network/common/eventqueue.go +++ b/pkg/network/common/eventqueue.go @@ -1,5 +1,3 @@ -// +build linux - package common import ( diff --git a/pkg/network/common/eventqueue_test.go b/pkg/network/common/eventqueue_test.go index b713d13eac11..ed71aa433cef 100644 --- a/pkg/network/common/eventqueue_test.go +++ b/pkg/network/common/eventqueue_test.go @@ -1,5 +1,3 @@ -// +build linux - package common import ( diff --git a/pkg/network/master/master.go b/pkg/network/master/master.go index 3159e4bff951..39eab970009b 100644 --- a/pkg/network/master/master.go +++ b/pkg/network/master/master.go @@ -1,5 +1,3 @@ -// +build linux - package master import ( @@ -22,10 +20,13 @@ import ( osapivalidation "github.com/openshift/origin/pkg/network/apis/network/validation" "github.com/openshift/origin/pkg/network/common" networkclient "github.com/openshift/origin/pkg/network/generated/internalclientset" - "github.com/openshift/origin/pkg/network/node" "github.com/openshift/origin/pkg/util/netutils" ) +const ( + tun0 = "tun0" +) + type OsdnMaster struct { kClient kclientset.Interface networkClient networkclient.Interface @@ -147,7 +148,7 @@ func Start(networkConfig osconfigapi.MasterNetworkConfig, networkClient networkc } func (master *OsdnMaster) checkClusterNetworkAgainstLocalNetworks() error { - hostIPNets, _, err := netutils.GetHostIPNetworks([]string{node.Tun0}) + hostIPNets, _, err := netutils.GetHostIPNetworks([]string{tun0}) if err != nil { return err } diff --git a/pkg/network/master/master_test.go b/pkg/network/master/master_test.go index 929f002c224f..b761ac9e31af 100644 --- a/pkg/network/master/master_test.go +++ b/pkg/network/master/master_test.go @@ -1,5 +1,3 @@ -// +build linux - package master import ( diff --git a/pkg/network/master/netid/allocator.go b/pkg/network/master/netid/allocator.go index 300d066926ad..dd2229df18d3 100644 --- a/pkg/network/master/netid/allocator.go +++ b/pkg/network/master/netid/allocator.go @@ -1,5 +1,3 @@ -// +build linux - package netid import ( diff --git a/pkg/network/master/netid/allocator_test.go b/pkg/network/master/netid/allocator_test.go index 284bd84be76c..07f44094cef5 100644 --- a/pkg/network/master/netid/allocator_test.go +++ b/pkg/network/master/netid/allocator_test.go @@ -1,5 +1,3 @@ -// +build linux - package netid import ( diff --git a/pkg/network/master/netid/netid.go b/pkg/network/master/netid/netid.go index dc4fc373b381..7b0e6bf5059a 100644 --- a/pkg/network/master/netid/netid.go +++ b/pkg/network/master/netid/netid.go @@ -1,5 +1,3 @@ -// +build linux - package netid import ( diff --git a/pkg/network/master/netid/netid_test.go b/pkg/network/master/netid/netid_test.go index 4956e837d9b0..2245902476b6 100644 --- a/pkg/network/master/netid/netid_test.go +++ b/pkg/network/master/netid/netid_test.go @@ -1,5 +1,3 @@ -// +build linux - package netid import ( diff --git a/pkg/network/master/subnets.go b/pkg/network/master/subnets.go index 4dd2bef016cd..cbcebf13e201 100644 --- a/pkg/network/master/subnets.go +++ b/pkg/network/master/subnets.go @@ -1,5 +1,3 @@ -// +build linux - package master import ( diff --git a/pkg/network/master/vnids.go b/pkg/network/master/vnids.go index 7292a4e72b53..7d8c62ec15e7 100644 --- a/pkg/network/master/vnids.go +++ b/pkg/network/master/vnids.go @@ -1,5 +1,3 @@ -// +build linux - package master import ( diff --git a/pkg/network/master/vnids_test.go b/pkg/network/master/vnids_test.go index 25b8042bdb47..1698a155ac7d 100644 --- a/pkg/network/master/vnids_test.go +++ b/pkg/network/master/vnids_test.go @@ -1,5 +1,3 @@ -// +build linux - package master import ( From 737d65b7cbb57d73e791090231bc3e678397452d Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 25 Sep 2017 22:03:15 -0400 Subject: [PATCH 07/11] Set a default certificate duration for bootstrapping CFSSL throws an opaque error, and bootstrapping requires user intervention to configure anyway. --- pkg/cmd/server/start/start_kube_controller_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/server/start/start_kube_controller_manager.go b/pkg/cmd/server/start/start_kube_controller_manager.go index 28ea196b3421..f45e70a1f997 100644 --- a/pkg/cmd/server/start/start_kube_controller_manager.go +++ b/pkg/cmd/server/start/start_kube_controller_manager.go @@ -100,7 +100,7 @@ func newKubeControllerManager(kubeconfigFile, saPrivateKeyFile, saRootCAFile, po cmdLineArgs["cluster-signing-key-file"] = []string{""} } if _, ok := cmdLineArgs["experimental-cluster-signing-duration"]; !ok { - cmdLineArgs["experimental-cluster-signing-duration"] = []string{"0s"} + cmdLineArgs["experimental-cluster-signing-duration"] = []string{"720h"} } if _, ok := cmdLineArgs["leader-elect-retry-period"]; !ok { cmdLineArgs["leader-elect-retry-period"] = []string{"3s"} From a04e49439a31dd19c80fd7f1de6013bee8f4f143 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 25 Sep 2017 22:04:48 -0400 Subject: [PATCH 08/11] Add --bootstrap-config-name to kubelet This allows the kubelet to be configured to load default configuration out of a known namespace. By default it is openshift-node/node-config. Correct an error in bootstrapping where errors weren't logged, and properly ignore forbidden errors when trying to load the config map. Add a better description of bootstrapping to openshift start node. Ensure the volume directory is correctly loaded from node-config during bootstrapping instead of being overwritten into the config directory. Enable client and server rotation on the node automatically when bootstrapping, and only do a client certificate creation (server bootstrapping done by kubelet only). This unfortunately requires setting a fake value in the node config that will be cleared later - as we are moving towards a future where node-config does not exist this entire section will likely go away. Relax validation on node-config to allow cert-dir to be provided instead of explicit certificates. bootstrap --- contrib/completions/bash/openshift | 4 +- contrib/completions/zsh/openshift | 4 +- pkg/cmd/server/api/validation/etcd.go | 4 +- pkg/cmd/server/api/validation/node.go | 14 +- pkg/cmd/server/api/validation/validation.go | 6 +- .../server/api/validation/validation_test.go | 2 +- .../server/kubernetes/node/options/options.go | 16 + pkg/cmd/server/start/bootstrap_node.go | 319 ++++++------------ pkg/cmd/server/start/node_args.go | 24 +- pkg/cmd/server/start/start_node.go | 39 ++- 10 files changed, 191 insertions(+), 241 deletions(-) diff --git a/contrib/completions/bash/openshift b/contrib/completions/bash/openshift index c7ec92eaad33..6363955429d8 100644 --- a/contrib/completions/bash/openshift +++ b/contrib/completions/bash/openshift @@ -33731,8 +33731,8 @@ _openshift_start_node() flags_with_completion=() flags_completion=() - flags+=("--bootstrap") - local_nonpersistent_flags+=("--bootstrap") + flags+=("--bootstrap-config-name=") + local_nonpersistent_flags+=("--bootstrap-config-name=") flags+=("--config=") flags_with_completion+=("--config") flags_completion+=("__handle_filename_extension_flag yaml|yml") diff --git a/contrib/completions/zsh/openshift b/contrib/completions/zsh/openshift index b19123164a1c..efa981b7272a 100644 --- a/contrib/completions/zsh/openshift +++ b/contrib/completions/zsh/openshift @@ -33880,8 +33880,8 @@ _openshift_start_node() flags_with_completion=() flags_completion=() - flags+=("--bootstrap") - local_nonpersistent_flags+=("--bootstrap") + flags+=("--bootstrap-config-name=") + local_nonpersistent_flags+=("--bootstrap-config-name=") flags+=("--config=") flags_with_completion+=("--config") flags_completion+=("__handle_filename_extension_flag yaml|yml") diff --git a/pkg/cmd/server/api/validation/etcd.go b/pkg/cmd/server/api/validation/etcd.go index 2a2b7ee842d6..8676c7b5b107 100644 --- a/pkg/cmd/server/api/validation/etcd.go +++ b/pkg/cmd/server/api/validation/etcd.go @@ -55,7 +55,7 @@ func ValidateEtcdConfig(config *api.EtcdConfig, fldPath *field.Path) ValidationR validationResults := ValidationResults{} servingInfoPath := fldPath.Child("servingInfo") - validationResults.Append(ValidateServingInfo(config.ServingInfo, servingInfoPath)) + validationResults.Append(ValidateServingInfo(config.ServingInfo, true, servingInfoPath)) if config.ServingInfo.BindNetwork == "tcp6" { validationResults.AddErrors(field.Invalid(servingInfoPath.Child("bindNetwork"), config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for etcd, must be tcp or tcp4")) } @@ -64,7 +64,7 @@ func ValidateEtcdConfig(config *api.EtcdConfig, fldPath *field.Path) ValidationR } peerServingInfoPath := fldPath.Child("peerServingInfo") - validationResults.Append(ValidateServingInfo(config.PeerServingInfo, peerServingInfoPath)) + validationResults.Append(ValidateServingInfo(config.PeerServingInfo, true, peerServingInfoPath)) if config.ServingInfo.BindNetwork == "tcp6" { validationResults.AddErrors(field.Invalid(peerServingInfoPath.Child("bindNetwork"), config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for etcd peers, must be tcp or tcp4")) } diff --git a/pkg/cmd/server/api/validation/node.go b/pkg/cmd/server/api/validation/node.go index 31ebd49fb0ea..5c908ba13f5c 100644 --- a/pkg/cmd/server/api/validation/node.go +++ b/pkg/cmd/server/api/validation/node.go @@ -12,6 +12,16 @@ import ( ) func ValidateNodeConfig(config *api.NodeConfig, fldPath *field.Path) ValidationResults { + validationResults := ValidateInClusterNodeConfig(config, fldPath) + if bootstrap := config.KubeletArguments["bootstrap-kubeconfig"]; len(bootstrap) > 0 { + validationResults.AddErrors(ValidateKubeConfig(bootstrap[0], fldPath.Child("kubeletArguments", "bootstrap-kubeconfig"))...) + } else { + validationResults.AddErrors(ValidateKubeConfig(config.MasterKubeConfig, fldPath.Child("masterKubeConfig"))...) + } + return validationResults +} + +func ValidateInClusterNodeConfig(config *api.NodeConfig, fldPath *field.Path) ValidationResults { validationResults := ValidationResults{} if len(config.NodeName) == 0 { @@ -22,11 +32,11 @@ func ValidateNodeConfig(config *api.NodeConfig, fldPath *field.Path) ValidationR } servingInfoPath := fldPath.Child("servingInfo") - validationResults.Append(ValidateServingInfo(config.ServingInfo, servingInfoPath)) + hasCertDir := len(config.KubeletArguments["cert-dir"]) > 0 + validationResults.Append(ValidateServingInfo(config.ServingInfo, !hasCertDir, servingInfoPath)) if config.ServingInfo.BindNetwork == "tcp6" { validationResults.AddErrors(field.Invalid(servingInfoPath.Child("bindNetwork"), config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for nodes, must be tcp or tcp4")) } - validationResults.AddErrors(ValidateKubeConfig(config.MasterKubeConfig, fldPath.Child("masterKubeConfig"))...) if len(config.DNSBindAddress) > 0 { validationResults.AddErrors(ValidateHostPort(config.DNSBindAddress, fldPath.Child("dnsBindAddress"))...) diff --git a/pkg/cmd/server/api/validation/validation.go b/pkg/cmd/server/api/validation/validation.go index b7a8619db34b..3999f6b562d6 100644 --- a/pkg/cmd/server/api/validation/validation.go +++ b/pkg/cmd/server/api/validation/validation.go @@ -105,11 +105,11 @@ func ValidateCertInfo(certInfo api.CertInfo, required bool, fldPath *field.Path) return allErrs } -func ValidateServingInfo(info api.ServingInfo, fldPath *field.Path) ValidationResults { +func ValidateServingInfo(info api.ServingInfo, certificatesRequired bool, fldPath *field.Path) ValidationResults { validationResults := ValidationResults{} validationResults.AddErrors(ValidateHostPort(info.BindAddress, fldPath.Child("bindAddress"))...) - validationResults.AddErrors(ValidateCertInfo(info.ServerCert, true, fldPath)...) + validationResults.AddErrors(ValidateCertInfo(info.ServerCert, certificatesRequired, fldPath)...) if len(info.NamedCertificates) > 0 && len(info.ServerCert.CertFile) == 0 { validationResults.AddErrors(field.Invalid(fldPath.Child("namedCertificates"), "", "a default certificate and key is required in certFile/keyFile in order to use namedCertificates")) @@ -221,7 +221,7 @@ func ValidateNamedCertificates(fldPath *field.Path, namedCertificates []api.Name func ValidateHTTPServingInfo(info api.HTTPServingInfo, fldPath *field.Path) ValidationResults { validationResults := ValidationResults{} - validationResults.Append(ValidateServingInfo(info.ServingInfo, fldPath)) + validationResults.Append(ValidateServingInfo(info.ServingInfo, true, fldPath)) if info.MaxRequestsInFlight < 0 { validationResults.AddErrors(field.Invalid(fldPath.Child("maxRequestsInFlight"), info.MaxRequestsInFlight, "must be zero (no limit) or greater")) diff --git a/pkg/cmd/server/api/validation/validation_test.go b/pkg/cmd/server/api/validation/validation_test.go index 8799646679d4..34866d7c762b 100644 --- a/pkg/cmd/server/api/validation/validation_test.go +++ b/pkg/cmd/server/api/validation/validation_test.go @@ -179,7 +179,7 @@ func TestValidateServingInfo(t *testing.T) { } for k, tc := range testcases { - result := ValidateServingInfo(tc.ServingInfo, nil) + result := ValidateServingInfo(tc.ServingInfo, true, nil) if len(tc.ExpectedErrors) != len(result.Errors) { t.Errorf("%s: Expected %d errors, got %d", k, len(tc.ExpectedErrors), len(result.Errors)) diff --git a/pkg/cmd/server/kubernetes/node/options/options.go b/pkg/cmd/server/kubernetes/node/options/options.go index 5039e557b99b..f526ba3bf0f0 100644 --- a/pkg/cmd/server/kubernetes/node/options/options.go +++ b/pkg/cmd/server/kubernetes/node/options/options.go @@ -8,9 +8,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kerrors "k8s.io/apimachinery/pkg/util/errors" + utilfeature "k8s.io/apiserver/pkg/util/feature" kubeproxyoptions "k8s.io/kubernetes/cmd/kube-proxy/app" kubeletoptions "k8s.io/kubernetes/cmd/kubelet/app/options" "k8s.io/kubernetes/pkg/apis/componentconfig" + "k8s.io/kubernetes/pkg/features" kubeletcni "k8s.io/kubernetes/pkg/kubelet/network/cni" kubelettypes "k8s.io/kubernetes/pkg/kubelet/types" @@ -122,6 +124,20 @@ func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, *compon return nil, nil, kerrors.NewAggregate(err) } + // terminate early if feature gate is incorrect on the node + if len(server.FeatureGates) > 0 { + if err := utilfeature.DefaultFeatureGate.Set(server.FeatureGates); err != nil { + return nil, nil, err + } + } + if utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletServerCertificate) { + // Server cert rotation is ineffective if a cert is hardcoded. + if len(server.CertDirectory) > 0 { + server.TLSCertFile = "" + server.TLSPrivateKeyFile = "" + } + } + proxyconfig, err := buildKubeProxyConfig(options) if err != nil { return nil, nil, err diff --git a/pkg/cmd/server/start/bootstrap_node.go b/pkg/cmd/server/start/bootstrap_node.go index b741e6122010..7199149bd76a 100644 --- a/pkg/cmd/server/start/bootstrap_node.go +++ b/pkg/cmd/server/start/bootstrap_node.go @@ -1,199 +1,69 @@ package start import ( - "crypto/rsa" - "crypto/tls" - "crypto/x509/pkix" "fmt" "io/ioutil" "os" "path/filepath" + "strings" "github.com/golang/glog" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/tools/clientcmd" utilcert "k8s.io/client-go/util/cert" kubeletapp "k8s.io/kubernetes/cmd/kubelet/app" - "k8s.io/kubernetes/pkg/apis/certificates" kclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" - clientcertificates "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/certificates/internalversion" + nodeutil "k8s.io/kubernetes/pkg/util/node" + configapi "github.com/openshift/origin/pkg/cmd/server/api" configapilatest "github.com/openshift/origin/pkg/cmd/server/api/latest" - "github.com/openshift/origin/pkg/cmd/server/crypto" + cmdutil "github.com/openshift/origin/pkg/cmd/util" ) -// readOrCreatePrivateKey attempts to read an rsa private key from the provided path, -// or if that fails, to generate a new private key. -func readOrCreatePrivateKey(path string) (*rsa.PrivateKey, error) { - if data, err := ioutil.ReadFile(path); err == nil { - if key, err := utilcert.ParsePrivateKeyPEM(data); err == nil { - if pkey, ok := key.(*rsa.PrivateKey); ok { - return pkey, nil - } - } - } - return utilcert.NewPrivateKey() -} - -// requestCertificate will create a certificate signing request using the PEM -// encoded CSR and send it to API server, then it will watch the object's -// status, once approved by API server, it will return the API server's issued -// certificate (pem-encoded). If there is any errors, or the watch timeouts, it -// will return an error. -func requestCertificate(client clientcertificates.CertificateSigningRequestInterface, csrData []byte, usages []certificates.KeyUsage) (certData []byte, err error) { - req, err := client.Create(&certificates.CertificateSigningRequest{ - // Username, UID, Groups will be injected by API server. - ObjectMeta: metav1.ObjectMeta{GenerateName: "csr-"}, - - Spec: certificates.CertificateSigningRequestSpec{ - Request: csrData, - Usages: usages, - }, - }) - if err != nil { - return nil, fmt.Errorf("cannot create certificate signing request: %v", err) - } - - // Make a default timeout = 3600s. - var defaultTimeoutSeconds int64 = 3600 - certWatch, err := client.Watch(metav1.ListOptions{ - Watch: true, - TimeoutSeconds: &defaultTimeoutSeconds, - FieldSelector: fields.OneTermEqualSelector("metadata.name", req.Name).String(), - }) - if err != nil { - return nil, fmt.Errorf("cannot watch on the certificate signing request: %v", err) - } - - var certificateData []byte - _, err = watch.Until(0, certWatch, func(event watch.Event) (bool, error) { - if event.Type != watch.Modified && event.Type != watch.Added { - return false, nil - } - if event.Object.(*certificates.CertificateSigningRequest).UID != req.UID { - return false, nil - } - - status := event.Object.(*certificates.CertificateSigningRequest).Status - for _, c := range status.Conditions { - if c.Type == certificates.CertificateDenied { - return false, fmt.Errorf("certificate signing request is not approved, reason: %v, message: %v", c.Reason, c.Message) - } - if c.Type == certificates.CertificateApproved && status.Certificate != nil { - certificateData = status.Certificate - return true, nil - } - } - return false, nil - }) - return certificateData, err -} - -// loadBootstrapServerCertificate attempts to read a server certificate file from the config dir, -// and otherwise tries to request a server certificate for its registered addresses. It will -// reuse a private key if one exists, and exit with an error if the CSR is not completed within -// timeout or if the current CSR does not validate against the local private key. -func loadBootstrapServerCertificate(nodeConfigDir string, hostnames []string, c kclientset.Interface) error { - glog.V(2).Info("Using node kubeconfig to generate TLS server cert and key file") - - serverCertPath := filepath.Join(nodeConfigDir, "server.crt") - serverKeyPath := filepath.Join(nodeConfigDir, "server.key") - - if _, err := os.Stat(serverCertPath); err == nil { - if _, err := os.Stat(serverKeyPath); err == nil { - if _, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath); err != nil { - return fmt.Errorf("bootstrap server certificate does not match private key: %v", err) - } - // continue - return nil - } - } - - privateKey, err := readOrCreatePrivateKey(serverKeyPath) - if err != nil { - return err - } - privateKeyData := utilcert.EncodePrivateKeyPEM(privateKey) - ipAddresses, dnsNames := crypto.IPAddressesDNSNames(hostnames) - csrData, err := utilcert.MakeCSR(privateKey, &pkix.Name{ - CommonName: dnsNames[0], - // TODO: indicate usage for server - }, dnsNames, ipAddresses) - if err != nil { - return err - } - - serverCertData, err := requestCertificate( - c.Certificates().CertificateSigningRequests(), - csrData, - []certificates.KeyUsage{ - // https://tools.ietf.org/html/rfc5280#section-4.2.1.3 - // - // Digital signature allows the certificate to be used to verify - // digital signatures used during TLS negotiation. - certificates.UsageDigitalSignature, - // KeyEncipherment allows the cert/key pair to be used to encrypt - // keys, including the symetric keys negotiated during TLS setup - // and used for data transfer. - certificates.UsageKeyEncipherment, - // ServerAuth allows the cert to be used by a TLS server to - // authenticate itself to a TLS client. - certificates.UsageServerAuth, - }, - ) - if err != nil { - return err - } - if err := ioutil.WriteFile(serverKeyPath, privateKeyData, 0600); err != nil { - return err - } - if err := ioutil.WriteFile(serverCertPath, serverCertData, 0600); err != nil { - return err - } - if _, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath); err != nil { - return fmt.Errorf("bootstrap server certificate does not match private key, you may need to delete the server CSR: %v", err) - } - - return nil -} - // loadBootstrap attempts to ensure a bootstrap configuration exists inside the node config dir -// by contacting the server and requesting a client and server certificate. If successful, it -// will attempt to download a node config file from namespace openshift-infra from the node-config -// ConfigMap. If no configuration is found, a new node config will be generated from the arguments -// and used instead. If no error is returned, nodeConfigDir can be used as a valid node configuration. -func (o NodeOptions) loadBootstrap(hostnames []string, nodeConfigDir string) error { +// by contacting the server and requesting a client certificate. If successful, it +// will attempt to download a node config file from namespace openshift-node from the node-config +// ConfigMap. If no error is returned, nodeConfigDir can be used as a valid node configuration. +// The actions of this method are intended to emulate the behavior of the kubelet during startup +// and will eventually be replaced by a pure Kubelet bootstrap. Bootstrap mode *requires* server +// certificate rotation to be enabled because it generates no server certificate. +func (o NodeOptions) loadBootstrap(nodeConfigDir string) error { if err := os.MkdirAll(nodeConfigDir, 0700); err != nil { return err } + // Emulate Kubelet bootstrapping - this codepath will be removed in a future release + // when we adopt dynamic config in the Kubelet. + bootstrapKubeconfig := o.NodeArgs.KubeConnectionArgs.ClientConfigLoadingRules.ExplicitPath nodeKubeconfig := filepath.Join(nodeConfigDir, "node.kubeconfig") - + certDir := filepath.Join(nodeConfigDir, "certificates") if err := kubeletapp.BootstrapClientCert( nodeKubeconfig, - o.NodeArgs.KubeConnectionArgs.ClientConfigLoadingRules.ExplicitPath, - nodeConfigDir, + bootstrapKubeconfig, + certDir, types.NodeName(o.NodeArgs.NodeName), ); err != nil { return err } - + if err := os.MkdirAll(certDir, 0600); err != nil { + return fmt.Errorf("unable to create kubelet certificate directory: %v", err) + } kubeClientConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(&clientcmd.ClientConfigLoadingRules{ExplicitPath: nodeKubeconfig}, &clientcmd.ConfigOverrides{}).ClientConfig() if err != nil { return err } + // clear the current client from the cert dir to ensure that the next rotation captures a new state + if err := os.Remove(filepath.Join(certDir, "kubelet-client-current.pem")); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("unable to remove current client pem for bootstrapping: %v", err) + } + c, err := kclientset.NewForConfig(kubeClientConfig) if err != nil { return err } - if err := loadBootstrapServerCertificate(nodeConfigDir, hostnames, c); err != nil { - return err - } nodeClientCAPath := filepath.Join(nodeConfigDir, "node-client-ca.crt") if err := utilcert.WriteCert(nodeClientCAPath, kubeClientConfig.CAData); err != nil { @@ -202,75 +72,98 @@ func (o NodeOptions) loadBootstrap(hostnames []string, nodeConfigDir string) err // try to refresh the latest node-config.yaml o.ConfigFile = filepath.Join(nodeConfigDir, "node-config.yaml") - config, err := c.Core().ConfigMaps("kube-system").Get("node-config", metav1.GetOptions{}) - if err == nil { - // skip all the config we generated ourselves - skipConfig := map[string]struct{}{"server.crt": {}, "server.key": {}, "master-client.crt": {}, "master-client.key": {}, "node.kubeconfig": {}} - for k, v := range config.Data { - if _, ok := skipConfig[k]; ok { - continue + config, err := c.Core().ConfigMaps(o.NodeArgs.BootstrapConfigNamespace).Get(o.NodeArgs.BootstrapConfigName, metav1.GetOptions{}) + if err != nil { + if kerrors.IsForbidden(err) { + glog.Warningf("Node is not authorized to access master, treating bootstrap configuration as invalid and exiting: %v", err) + if err := os.Remove(nodeKubeconfig); err != nil && !os.IsNotExist(err) { + glog.Warningf("Unable to remove bootstrap client configuration: %v", err) } - b := []byte(v) - // if a node config is provided, override the setup. - if k == "node-config.yaml" { - if err := ioutil.WriteFile(o.ConfigFile, []byte(v), 0600); err != nil { - return err - } - nodeConfig, err := configapilatest.ReadNodeConfig(o.ConfigFile) - if err != nil { - return err - } - if err := o.NodeArgs.MergeSerializeableNodeConfig(nodeConfig); err != nil { - return err - } - nodeConfig.ServingInfo.ServerCert.CertFile = filepath.Join(nodeConfigDir, "server.crt") - nodeConfig.ServingInfo.ServerCert.KeyFile = filepath.Join(nodeConfigDir, "server.key") - nodeConfig.ServingInfo.ClientCA = nodeClientCAPath - nodeConfig.MasterKubeConfig = filepath.Join(nodeConfigDir, "node.kubeconfig") - b, err = configapilatest.WriteYAML(nodeConfig) - if err != nil { - return err - } + return err + } + glog.Warningf("Node is unable to access config, exiting: %v", err) + return err + } + + glog.V(2).Infof("Loading node configuration from %s/%s (rv=%s, uid=%s)", config.Namespace, config.Name, config.ResourceVersion, config.UID) + // skip all the config we generated ourselves + var loaded []string + skipConfig := map[string]struct{}{ + "server.crt": {}, + "server.key": {}, + "master-client.crt": {}, + "master-client.key": {}, + "node.kubeconfig": {}, + } + for k, v := range config.Data { + if _, ok := skipConfig[k]; ok { + glog.V(2).Infof("Skipping key %q from config map", k) + continue + } + b := []byte(v) + // if a node config is provided, override the setup. + if k == "node-config.yaml" { + if err := ioutil.WriteFile(o.ConfigFile, []byte(v), 0600); err != nil { + return err } + nodeConfig, err := configapilatest.ReadNodeConfig(o.ConfigFile) + if err != nil { + return err + } + if err := o.NodeArgs.MergeSerializeableNodeConfig(nodeConfig); err != nil { + return err + } + + overrideNodeConfigForBootstrap(nodeConfig, bootstrapKubeconfig) - if err := ioutil.WriteFile(filepath.Join(nodeConfigDir, k), b, 0600); err != nil { + b, err = configapilatest.WriteYAML(nodeConfig) + if err != nil { return err } } - glog.V(3).Infof("Received %d bootstrap files into %s", len(config.Data), nodeConfigDir) + loaded = append(loaded, k) + if err := ioutil.WriteFile(filepath.Join(nodeConfigDir, k), b, 0600); err != nil { + return err + } } + glog.V(3).Infof("Received bootstrap files into %s: %s", nodeConfigDir, strings.Join(loaded, ", ")) + return nil +} - // if we had a previous node-config.yaml, continue using it - if _, err2 := os.Stat(o.ConfigFile); err2 == nil { - if err == nil { - glog.V(2).Infof("Unable to load node configuration from the server: %v", err) - } - return nil +// overrideNodeConfigForBootstrap sets certain bootstrap overrides. +func overrideNodeConfigForBootstrap(nodeConfig *configapi.NodeConfig, bootstrapKubeconfig string) { + if nodeConfig.KubeletArguments == nil { + nodeConfig.KubeletArguments = configapi.ExtendedArguments{} } - // if there is no node-config.yaml and no server config map, generate one - if kerrors.IsNotFound(err) { - glog.V(2).Infof("Generating a local configuration since no server config available") - nodeConfig, err := o.NodeArgs.BuildSerializeableNodeConfig() - if err != nil { - return err - } - if err := o.NodeArgs.MergeSerializeableNodeConfig(nodeConfig); err != nil { - return err - } - nodeConfig.ServingInfo.ServerCert.CertFile = "server.crt" - nodeConfig.ServingInfo.ServerCert.KeyFile = "server.key" - nodeConfig.ServingInfo.ClientCA = "node-client-ca.crt" - nodeConfig.MasterKubeConfig = "node.kubeconfig" - b, err := configapilatest.WriteYAML(nodeConfig) - if err != nil { - return err - } - if err := ioutil.WriteFile(o.ConfigFile, b, 0600); err != nil { - return err + // Set impliict defaults the same as the kubelet (until this entire code path is removed) + nodeConfig.NodeName = nodeutil.GetHostname(nodeConfig.NodeName) + if nodeConfig.DNSIP == "0.0.0.0" { + nodeConfig.DNSIP = nodeConfig.NodeIP + // TODO: the Kubelet should do this defaulting (to the IP it recognizes) + if len(nodeConfig.DNSIP) == 0 { + if ip, err := cmdutil.DefaultLocalIP4(); err == nil { + nodeConfig.DNSIP = ip.String() + } } - return nil } - return err + // Created during bootstrapping + nodeConfig.ServingInfo.ClientCA = "node-client-ca.crt" + + // We will use cert-dir instead and bootstrapping + nodeConfig.ServingInfo.ServerCert.CertFile = "" + nodeConfig.ServingInfo.ServerCert.KeyFile = "" + nodeConfig.KubeletArguments["bootstrap-kubeconfig"] = []string{bootstrapKubeconfig} + + // Default a valid certificate directory to store bootstrap certs + if _, ok := nodeConfig.KubeletArguments["cert-dir"]; !ok { + nodeConfig.KubeletArguments["cert-dir"] = []string{"./certificates"} + } + // Enable both client and server rotation when bootstrapping + if _, ok := nodeConfig.KubeletArguments["feature-gates"]; !ok { + nodeConfig.KubeletArguments["feature-gates"] = []string{ + "RotateKubeletClientCertificate=true,RotateKubeletServerCertificate=true", + } + } } diff --git a/pkg/cmd/server/start/node_args.go b/pkg/cmd/server/start/node_args.go index 82bf8117ae11..b67a6a842fb6 100644 --- a/pkg/cmd/server/start/node_args.go +++ b/pkg/cmd/server/start/node_args.go @@ -57,6 +57,10 @@ type NodeArgs struct { // Bootstrap is true if the node should rely on the server to set initial configuration. Bootstrap bool + // BootstrapConfigName is the name of a config map to read node-config.yaml from. + BootstrapConfigName string + // BootstrapConfigNamespace is the namespace the config map for bootstrap config is expected to load from. + BootstrapConfigNamespace string MasterCertDir string ConfigDir flag.StringFlag @@ -64,6 +68,8 @@ type NodeArgs struct { AllowDisabledDocker bool // VolumeDir is the volume storage directory. VolumeDir string + // VolumeDirProvided is set to true if the user has specified this flag. + VolumeDirProvided bool DefaultKubernetesURL *url.URL ClusterDomain string @@ -128,6 +134,9 @@ func NewDefaultNodeArgs() *NodeArgs { NodeName: hostname, + BootstrapConfigName: "", + BootstrapConfigNamespace: "openshift-node", + MasterCertDir: "openshift.local.config/master", ClusterDomain: cmdutil.Env("OPENSHIFT_DNS_DOMAIN", "cluster.local"), @@ -151,6 +160,11 @@ func (args NodeArgs) Validate() error { if addr, _ := args.KubeConnectionArgs.GetKubernetesAddress(args.DefaultKubernetesURL); addr == nil { return errors.New("--kubeconfig must be set to provide API server connection information") } + if len(args.BootstrapConfigName) > 0 { + if len(args.BootstrapConfigNamespace) == 0 { + return errors.New("--bootstrap-config-namespace must be specified") + } + } return nil } @@ -233,10 +247,12 @@ func (args NodeArgs) MergeSerializeableNodeConfig(config *configapi.NodeConfig) if len(args.NodeName) > 0 { config.NodeName = args.NodeName } - - config.ServingInfo.BindAddress = net.JoinHostPort(args.ListenArg.ListenAddr.Host, strconv.Itoa(ports.KubeletPort)) - - config.VolumeDirectory = args.VolumeDir + if args.ListenArg.ListenAddr.Provided { + config.ServingInfo.BindAddress = net.JoinHostPort(args.ListenArg.ListenAddr.Host, strconv.Itoa(ports.KubeletPort)) + } + if args.VolumeDirProvided { + config.VolumeDirectory = args.VolumeDir + } config.AllowDisabledDocker = args.AllowDisabledDocker return nil } diff --git a/pkg/cmd/server/start/start_node.go b/pkg/cmd/server/start/start_node.go index 74267b8ea444..068040649c58 100644 --- a/pkg/cmd/server/start/start_node.go +++ b/pkg/cmd/server/start/start_node.go @@ -52,7 +52,16 @@ var nodeLong = templates.LongDesc(` %[1]s start node --config= will start a node with given configuration file. The node will run in the - foreground until you terminate the process.`) + foreground until you terminate the process. + + The --bootstrap-config-name flag instructs the node to use the provided + kubeconfig file to contact the master and request a client cert (its identity) and + a serving cert, and then downloads node-config.yaml from the named config map. + If no config map exists in the openshift-node namespace the node will exit with + an error. In this mode --config will be location of the downloaded config. + Turning on bootstrapping will always use certificate rotation by default at the + master's preferred rotation interval. + `) // NewCommandStartNode provides a CLI handler for 'start node' command func NewCommandStartNode(basename string, out, errout io.Writer) (*cobra.Command, *NodeOptions) { @@ -81,7 +90,7 @@ func NewCommandStartNode(basename string, out, errout io.Writer) (*cobra.Command BindImageFormatArgs(options.NodeArgs.ImageFormatArgs, flags, "") BindKubeConnectionArgs(options.NodeArgs.KubeConnectionArgs, flags, "") - flags.BoolVar(&options.NodeArgs.Bootstrap, "bootstrap", false, "Use the provided .kubeconfig file to perform initial node setup (experimental).") + flags.StringVar(&options.NodeArgs.BootstrapConfigName, "bootstrap-config-name", options.NodeArgs.BootstrapConfigName, "On startup, the node will request a client cert from the master and get its config from this config map in the openshift-node namespace (experimental).") // autocompletion hints cmd.MarkFlagFilename("config", "yaml", "yml") @@ -128,7 +137,7 @@ func NewCommandStartNetwork(basename string, out, errout io.Writer) (*cobra.Comm } func (options *NodeOptions) Run(c *cobra.Command, errout io.Writer, args []string) { - kcmdutil.CheckErr(options.Complete()) + kcmdutil.CheckErr(options.Complete(c)) kcmdutil.CheckErr(options.Validate(args)) startProfiler() @@ -163,7 +172,7 @@ func (o NodeOptions) Validate(args []string) error { } // if we are starting up using a config file, run no validations here - if o.NodeArgs.Bootstrap && !o.IsRunFromConfig() { + if len(o.NodeArgs.BootstrapConfigName) > 0 && !o.IsRunFromConfig() { if err := o.NodeArgs.Validate(); err != nil { return err } @@ -172,9 +181,14 @@ func (o NodeOptions) Validate(args []string) error { return nil } -func (o NodeOptions) Complete() error { +func (o NodeOptions) Complete(cmd *cobra.Command) error { o.NodeArgs.NodeName = strings.ToLower(o.NodeArgs.NodeName) - + if len(o.ConfigFile) > 0 { + o.NodeArgs.ConfigDir.Default(filepath.Dir(o.ConfigFile)) + } + if flag := cmd.Flags().Lookup("volume-dir"); flag != nil { + o.NodeArgs.VolumeDirProvided = flag.Changed + } return nil } @@ -235,15 +249,11 @@ func (o NodeOptions) RunNode() error { // a string for messages indicating which config file contains the config. func (o NodeOptions) resolveNodeConfig() (*configapi.NodeConfig, string, error) { switch { - case o.NodeArgs.Bootstrap: + case len(o.NodeArgs.BootstrapConfigName) > 0: glog.V(2).Infof("Bootstrapping from master configuration") - hostnames, err := o.NodeArgs.GetServerCertHostnames() - if err != nil { - return nil, "", err - } nodeConfigDir := o.NodeArgs.ConfigDir.Value() - if err := o.loadBootstrap(hostnames.List(), nodeConfigDir); err != nil { + if err := o.loadBootstrap(nodeConfigDir); err != nil { return nil, "", err } configFile := o.ConfigFile @@ -369,6 +379,11 @@ func execKubelet(server *kubeletoptions.KubeletServer) (bool, error) { glog.Warningf("UNSUPPORTED: Executing a different Kubelet than the current binary is not supported: %s", kubeletPath) } + server.RootDirectory, err = filepath.Abs(server.RootDirectory) + if err != nil { + return false, fmt.Errorf("unable to set absolute path for Kubelet root directory: %v", err) + } + // convert current settings to flags args := nodeoptions.ToFlags(server) args = append([]string{kubeletPath}, args...) From c9db1544d5c7d921f3d0e50ce36d73f5ec31a84c Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Mon, 25 Sep 2017 23:27:45 -0400 Subject: [PATCH 09/11] Auto-create openshift-node and given nodes read on node-config Other config variants will be stored in this location. The new namespace ensures clean security isolation. --- pkg/cmd/server/bootstrappolicy/constants.go | 10 ++++-- .../bootstrappolicy/namespace_policy.go | 13 ++++++++ test/integration/front_proxy_test.go | 1 + .../bootstrap_namespace_role_bindings.yaml | 16 +++++++++ .../bootstrap_namespace_roles.yaml | 15 +++++++++ .../bootstrap_policy_file.yaml | 33 +++++++++++++++++++ 6 files changed, 85 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/server/bootstrappolicy/constants.go b/pkg/cmd/server/bootstrappolicy/constants.go index 4401ee901fe3..09a2e7574fcd 100644 --- a/pkg/cmd/server/bootstrappolicy/constants.go +++ b/pkg/cmd/server/bootstrappolicy/constants.go @@ -4,6 +4,7 @@ package bootstrappolicy const ( DefaultOpenShiftSharedResourcesNamespace = "openshift" DefaultOpenShiftInfraNamespace = "openshift-infra" + DefaultOpenShiftNodeNamespace = "openshift-node" ) // users @@ -98,11 +99,13 @@ const ( OpenshiftSharedResourceViewRoleName = "shared-resource-viewer" - NodeBootstrapRoleName = "system:node-bootstrapper" + NodeBootstrapRoleName = "system:node-bootstrapper" + NodeConfigReaderRoleName = "system:node-config-reader" ) // RoleBindings const ( + // Legacy roles that must continue to have a plural form SelfAccessReviewerRoleBindingName = SelfAccessReviewerRoleName + "s" SelfProvisionerRoleBindingName = SelfProvisionerRoleName + "s" DeployerRoleBindingName = DeployerRoleName + "s" @@ -128,10 +131,11 @@ const ( RegistryViewerRoleBindingName = RegistryViewerRoleName + "s" RegistryEditorRoleBindingName = RegistryEditorRoleName + "s" + OpenshiftSharedResourceViewRoleBindingName = OpenshiftSharedResourceViewRoleName + "s" + + // Bindings BuildStrategyDockerRoleBindingName = BuildStrategyDockerRoleName + "-binding" BuildStrategyCustomRoleBindingName = BuildStrategyCustomRoleName + "-binding" BuildStrategySourceRoleBindingName = BuildStrategySourceRoleName + "-binding" BuildStrategyJenkinsPipelineRoleBindingName = BuildStrategyJenkinsPipelineRoleName + "-binding" - - OpenshiftSharedResourceViewRoleBindingName = OpenshiftSharedResourceViewRoleName + "s" ) diff --git a/pkg/cmd/server/bootstrappolicy/namespace_policy.go b/pkg/cmd/server/bootstrappolicy/namespace_policy.go index b9e41a6848dd..a49855f6b7ff 100644 --- a/pkg/cmd/server/bootstrappolicy/namespace_policy.go +++ b/pkg/cmd/server/bootstrappolicy/namespace_policy.go @@ -67,6 +67,19 @@ func buildNamespaceRolesAndBindings() (map[string][]rbac.Role, map[string][]rbac DefaultOpenShiftSharedResourcesNamespace, newOriginRoleBinding(OpenshiftSharedResourceViewRoleBindingName, OpenshiftSharedResourceViewRoleName, DefaultOpenShiftSharedResourcesNamespace).Groups(AuthenticatedGroup).BindingOrDie()) + addNamespaceRole(namespaceRoles, + DefaultOpenShiftNodeNamespace, + rbac.Role{ + ObjectMeta: metav1.ObjectMeta{Name: NodeConfigReaderRoleName}, + Rules: []rbac.PolicyRule{ + // Allow the reader to read config maps in a given namespace with a given name. + rbac.NewRule("get").Groups(kapiGroup).Resources("configmaps").RuleOrDie(), + }, + }) + addNamespaceRoleBinding(namespaceRoleBindings, + DefaultOpenShiftNodeNamespace, + rbac.NewRoleBinding(NodeConfigReaderRoleName, DefaultOpenShiftNodeNamespace).Groups(NodesGroup).BindingOrDie()) + return namespaceRoles, namespaceRoleBindings } diff --git a/test/integration/front_proxy_test.go b/test/integration/front_proxy_test.go index d1617e17f753..f59292de663c 100644 --- a/test/integration/front_proxy_test.go +++ b/test/integration/front_proxy_test.go @@ -157,6 +157,7 @@ func TestFrontProxy(t *testing.T) { "kube-system", "openshift", "openshift-infra", + "openshift-node", ), }, } { diff --git a/test/testdata/bootstrappolicy/bootstrap_namespace_role_bindings.yaml b/test/testdata/bootstrappolicy/bootstrap_namespace_role_bindings.yaml index 9c5b6f64da72..4914e454bcc5 100644 --- a/test/testdata/bootstrappolicy/bootstrap_namespace_role_bindings.yaml +++ b/test/testdata/bootstrappolicy/bootstrap_namespace_role_bindings.yaml @@ -124,5 +124,21 @@ items: - apiGroup: rbac.authorization.k8s.io kind: Group name: system:authenticated +- apiVersion: rbac.authorization.k8s.io/v1beta1 + kind: RoleBinding + metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" + creationTimestamp: null + name: system:node-config-reader + namespace: openshift-node + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: system:node-config-reader + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:nodes kind: List metadata: {} diff --git a/test/testdata/bootstrappolicy/bootstrap_namespace_roles.yaml b/test/testdata/bootstrappolicy/bootstrap_namespace_roles.yaml index ad779c9b67f0..db92c0976ebc 100644 --- a/test/testdata/bootstrappolicy/bootstrap_namespace_roles.yaml +++ b/test/testdata/bootstrappolicy/bootstrap_namespace_roles.yaml @@ -209,5 +209,20 @@ items: - imagestreams/layers verbs: - get +- apiVersion: rbac.authorization.k8s.io/v1beta1 + kind: Role + metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" + creationTimestamp: null + name: system:node-config-reader + namespace: openshift-node + rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get kind: List metadata: {} diff --git a/test/testdata/bootstrappolicy/bootstrap_policy_file.yaml b/test/testdata/bootstrappolicy/bootstrap_policy_file.yaml index 64a59c34faf0..509882321caf 100644 --- a/test/testdata/bootstrappolicy/bootstrap_policy_file.yaml +++ b/test/testdata/bootstrappolicy/bootstrap_policy_file.yaml @@ -7086,6 +7086,22 @@ items: - imagestreams/layers verbs: - get +- apiVersion: v1 + kind: Role + metadata: + annotations: + openshift.io/reconcile-protect: "false" + creationTimestamp: null + name: system:node-config-reader + namespace: openshift-node + rules: + - apiGroups: + - "" + attributeRestrictions: null + resources: + - configmaps + verbs: + - get - apiVersion: v1 groupNames: null kind: RoleBinding @@ -7223,5 +7239,22 @@ items: - kind: SystemGroup name: system:authenticated userNames: null +- apiVersion: v1 + groupNames: + - system:nodes + kind: RoleBinding + metadata: + annotations: + openshift.io/reconcile-protect: "false" + creationTimestamp: null + name: system:node-config-reader + namespace: openshift-node + roleRef: + name: system:node-config-reader + namespace: openshift-node + subjects: + - kind: SystemGroup + name: system:nodes + userNames: null kind: List metadata: {} From c3d583056fe7a9af9e61731d0fd5b30b90f57dff Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Thu, 14 Sep 2017 10:16:47 -0400 Subject: [PATCH 10/11] Add a prototypical network-daemonset --- contrib/kubernetes/controllers.yaml | 26 --- contrib/kubernetes/default-node-config.yaml | 51 ++++++ .../kubernetes/static/controllers-pod.yaml | 2 +- .../kubernetes/static/network-daemonset.yaml | 157 ++++++++++++++++++ contrib/kubernetes/static/network-ovs.yaml | 61 +++++++ contrib/kubernetes/static/network-policy.yaml | 29 ++++ contrib/kubernetes/static/sign.sh | 33 ++++ pkg/cmd/server/start/start_node.go | 2 + 8 files changed, 334 insertions(+), 27 deletions(-) delete mode 100644 contrib/kubernetes/controllers.yaml create mode 100644 contrib/kubernetes/default-node-config.yaml create mode 100644 contrib/kubernetes/static/network-daemonset.yaml create mode 100644 contrib/kubernetes/static/network-ovs.yaml create mode 100644 contrib/kubernetes/static/network-policy.yaml create mode 100755 contrib/kubernetes/static/sign.sh diff --git a/contrib/kubernetes/controllers.yaml b/contrib/kubernetes/controllers.yaml deleted file mode 100644 index 378d9d8893e5..000000000000 --- a/contrib/kubernetes/controllers.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: controllers - labels: - master.openshift.io/controllers: 'true' -spec: - containers: - - name: controller - image: openshift/origin:latest - args: - - start - - master - - controllers - - --listen=0.0.0.0:8444 - - --config=/etc/origin/master/master-config.yaml - volumeMounts: - - name: config - mountPath: /etc/origin/master - ports: - - containerPort: 8444 - name: https - volumes: - - hostPath: - path: /data/src/github.com/openshift/origin/openshift.local.test/master - name: config diff --git a/contrib/kubernetes/default-node-config.yaml b/contrib/kubernetes/default-node-config.yaml new file mode 100644 index 000000000000..63b3e9a488f4 --- /dev/null +++ b/contrib/kubernetes/default-node-config.yaml @@ -0,0 +1,51 @@ +allowDisabledDocker: false +apiVersion: v1 +authConfig: + authenticationCacheSize: 1000 + authenticationCacheTTL: 5m + authorizationCacheSize: 1000 + authorizationCacheTTL: 5m +dnsDomain: cluster.local +dnsIP: 0.0.0.0 +dnsBindAddress: 0.0.0.0:53 +dnsRecursiveResolvConf: "" +dockerConfig: + dockerShimRootDirectory: /var/lib/dockershim + dockerShimSocket: /var/run/kubernetes/dockershim.sock + execHandlerName: native +enableUnidling: true +imageConfig: + format: openshift/origin-${component}:${version} + latest: false +iptablesSyncPeriod: 30s +kind: NodeConfig +kubeletArguments: + cert-dir: + - ./certificates + feature-gates: + - RotateKubeletClientCertificate=true,RotateKubeletServerCertificate=true +masterClientConnectionOverrides: + acceptContentTypes: application/vnd.kubernetes.protobuf,application/json + burst: 40 + contentType: application/vnd.kubernetes.protobuf + qps: 20 +masterKubeConfig: node.kubeconfig +networkConfig: + mtu: 1450 + networkPluginName: redhat/openshift-ovs-multitenant +nodeIP: "" +proxyArguments: + healthz-bind-address: + - 0.0.0.0 + healthz-port: + - "10256" + metrics-bind-address: + - 0.0.0.0:10257 +servingInfo: + bindAddress: 0.0.0.0:10250 + bindNetwork: tcp4 + namedCertificates: null +volumeConfig: + localQuota: + perFSGroup: null +volumeDirectory: /var/lib/origin/volumes diff --git a/contrib/kubernetes/static/controllers-pod.yaml b/contrib/kubernetes/static/controllers-pod.yaml index e012638ce2ce..f8998f1dca45 100644 --- a/contrib/kubernetes/static/controllers-pod.yaml +++ b/contrib/kubernetes/static/controllers-pod.yaml @@ -5,7 +5,7 @@ metadata: spec: containers: - name: controllers - image: openshift/origin:v3.6.0-rc.0 + image: openshift/origin:v3.6.0 command: ["/usr/bin/openshift", "start", "master", "controllers"] args: - "--config=/etc/origin/master/master-config.yaml" diff --git a/contrib/kubernetes/static/network-daemonset.yaml b/contrib/kubernetes/static/network-daemonset.yaml new file mode 100644 index 000000000000..829dbf12fccf --- /dev/null +++ b/contrib/kubernetes/static/network-daemonset.yaml @@ -0,0 +1,157 @@ +kind: DaemonSet +apiVersion: extensions/v1beta1 +metadata: + name: sdn + annotations: + kubernetes.io/description: | + This daemon set launches the OpenShift networking components (kube-proxy, DNS, and openshift-sdn). + It expects that OVS is running on the node. +spec: + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + component: network + type: infra + openshift.io/role: network + annotations: + scheduler.alpha.kubernetes.io/critical-pod: '' + spec: + # Requires fairly broad permissions - ability to read all services and network functions as well + # as all pods. + serviceAccountName: sdn + hostNetwork: true + hostPID: true + containers: + - name: network + image: openshift/node:v3.7.0-alpha.1 + command: + - /bin/bash + - -c + - | + #!/bin/sh + set -o errexit + # Take over network functions on the node + rm -Rf /etc/cni/net.d/* + rm -Rf /host/opt/cni/bin/* + cp -Rf /opt/cni/bin/* /host/opt/cni/bin/ + # Use whichever node-config exists + cfg=/etc/openshift/node + if [[ ! -f "${cfg}/node-config.yaml" ]]; then + cfg=/etc/origin/node + fi + # Use the same config as the node, but with the service account token + openshift cli config "--config=${cfg}/node.kubeconfig" view --flatten > /tmp/kubeconfig + openshift cli config --config=/tmp/kubeconfig set-credentials sa "--token=$( cat /var/run/secrets/kubernetes.io/serviceaccount/token )" + openshift cli config --config=/tmp/kubeconfig set-context "$( openshift cli config current-context)" --user=sa + # Launch the network process + exec openshift start network "--config=${cfg}/node-config.yaml" --kubeconfig=/tmp/kubeconfig --loglevel=5 + + securityContext: + runAsUser: 0 + # Permission could be reduced by selecting an appropriate SELinux policy + privileged: true + # TODO: debugging only + imagePullPolicy: Never + volumeMounts: + # Directory which contains the host configuration. We look at both locations + # to simplify setup. + - mountPath: /etc/origin/node/ + name: host-config + readOnly: true + - mountPath: /etc/openshift/node/ + name: host-config-alt + readOnly: true + # Run directories where we need to be able to access sockets + - mountPath: /var/run/dbus/ + name: host-var-run-dbus + readOnly: true + - mountPath: /var/run/openvswitch/ + name: host-var-run-ovs + readOnly: true + - mountPath: /var/run/kubernetes/ + name: host-var-run-kubernetes + readOnly: true + # We mount our socket here + - mountPath: /var/run/openshift-sdn + name: host-var-run-openshift-sdn + # CNI related mounts which we take over + - mountPath: /host/opt/cni/bin + name: host-opt-cni-bin + - mountPath: /etc/cni/net.d + name: host-etc-cni-netd + - mountPath: /var/lib/cni/networks/openshift-sdn + name: host-var-lib-cni-networks-openshift-sdn + + resources: + requests: + cpu: 100m + memory: 200Mi + env: + - name: OPENSHIFT_DNS_DOMAIN + value: cluster.local + ports: + - name: healthz + containerPort: 10256 + livenessProbe: + initialDelaySeconds: 10 + httpGet: + path: /healthz + port: 10256 + scheme: HTTP + lifecycle: + # postStart: + # exec: + # command: + # - /usr/bin/dbus-send + # - --system + # - --dest=uk.org.thekelleys.dnsmasq + # - /uk/org/thekelleys/dnsmasq + # - uk.org.thekelleys.SetDomainServers + # - array:string:/in-addr.arpa/127.0.0.1,/$(OPENSHIFT_DNS_DOMAIN)/127.0.0.1 + # preStop: + # exec: + # command: + # - /usr/bin/dbus-send + # - --system + # - --dest=uk.org.thekelleys.dnsmasq + # - /uk/org/thekelleys/dnsmasq + # - uk.org.thekelleys.SetDomainServers + # - "array:string:" + + volumes: + # In bootstrap mode, the host config contains information not easily available + # from other locations. + - name: host-config + hostPath: + path: /etc/origin/node + - name: host-config-alt + hostPath: + path: /etc/openshift/node + - name: host-modules + hostPath: + path: /lib/modules + + - name: host-var-run-ovs + hostPath: + path: /var/run/openvswitch + - name: host-var-run-kubernetes + hostPath: + path: /var/run/kubernetes + - name: host-var-run-dbus + hostPath: + path: /var/run/dbus + - name: host-var-run-openshift-sdn + hostPath: + path: /var/run/openshift-sdn + + - name: host-opt-cni-bin + hostPath: + path: /opt/cni/bin + - name: host-etc-cni-netd + hostPath: + path: /etc/cni/net.d + - name: host-var-lib-cni-networks-openshift-sdn + hostPath: + path: /var/lib/cni/networks/openshift-sdn diff --git a/contrib/kubernetes/static/network-ovs.yaml b/contrib/kubernetes/static/network-ovs.yaml new file mode 100644 index 000000000000..741851ccf5b4 --- /dev/null +++ b/contrib/kubernetes/static/network-ovs.yaml @@ -0,0 +1,61 @@ +kind: DaemonSet +apiVersion: extensions/v1beta1 +metadata: + name: ovs + annotations: + kubernetes.io/description: | + This daemon set launches the openvswitch daemon. +spec: + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + component: network + type: infra + openshift.io/role: network + annotations: + scheduler.alpha.kubernetes.io/critical-pod: '' + spec: + # Requires fairly broad permissions - ability to read all services and network functions as well + # as all pods. + serviceAccountName: sdn + hostNetwork: true + containers: + - name: openvswitch + image: openshift/openvswitch:v3.7.0-alpha.1 + securityContext: + runAsUser: 0 + privileged: true + volumeMounts: + - mountPath: /lib/modules + name: host-modules + readOnly: true + - mountPath: /run/openvswitch + name: host-run-ovs + - mountPath: /sys + name: host-sys + readOnly: true + - mountPath: /etc/openvswitch + name: host-config-openvswitch + resources: + requests: + cpu: 100m + memory: 200Mi + limits: + cpu: 200m + memory: 300Mi + + volumes: + - name: host-modules + hostPath: + path: /lib/modules + - name: host-run-ovs + hostPath: + path: /run/openvswitch + - name: host-sys + hostPath: + path: /sys + - name: host-config-openvswitch + hostPath: + path: /etc/origin/openvswitch diff --git a/contrib/kubernetes/static/network-policy.yaml b/contrib/kubernetes/static/network-policy.yaml new file mode 100644 index 000000000000..7304dea1f8ac --- /dev/null +++ b/contrib/kubernetes/static/network-policy.yaml @@ -0,0 +1,29 @@ +kind: List +apiVersion: v1 +items: +- kind: ServiceAccount + apiVersion: v1 + metadata: + name: sdn + namespace: openshift-node +- apiVersion: authorization.openshift.io/v1 + kind: ClusterRoleBinding + metadata: + name: sdn-cluster-reader + roleRef: + name: cluster-reader + subjects: + - kind: ServiceAccount + name: sdn + namespace: openshift-node +- apiVersion: authorization.openshift.io/v1 + kind: ClusterRoleBinding + metadata: + name: sdn-reader + roleRef: + name: system:sdn-reader + subjects: + - kind: ServiceAccount + name: sdn + namespace: openshift-node +# TODO: PSP binding \ No newline at end of file diff --git a/contrib/kubernetes/static/sign.sh b/contrib/kubernetes/static/sign.sh new file mode 100755 index 000000000000..cb61f8bca937 --- /dev/null +++ b/contrib/kubernetes/static/sign.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# +# This script is expected to be run with: +# +# $ oc observe csr -a '{.status.conditions[*].type}' -a '{.status.certificate}' -- PATH_TO_SCRIPT +# +# It will approve any CSR that is not approved yet, and delete any CSR that expired more than 60 seconds +# ago. +# + +set -o errexit +set -o nounset +set -o pipefail + +name=${1} +condition=${2} +certificate=${3} + +# auto approve +if [[ -z "${condition}" ]]; then + oc adm certificate approve "${name}" + exit 0 +fi + +# check certificate age +if [[ -n "${certificate}" ]]; then + text="$( echo "${certificate}" | base64 -D - )" + if ! echo "${text}" | openssl x509 -checkend -60 > /dev/null; then + echo "Certificate is expired, deleting" + oc delete csr "${name}" + fi + exit 0 +fi diff --git a/pkg/cmd/server/start/start_node.go b/pkg/cmd/server/start/start_node.go index 068040649c58..4373818586e1 100644 --- a/pkg/cmd/server/start/start_node.go +++ b/pkg/cmd/server/start/start_node.go @@ -21,6 +21,7 @@ import ( kubeletoptions "k8s.io/kubernetes/cmd/kubelet/app/options" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/master/ports" "github.com/openshift/origin/pkg/cmd/server/admin" configapi "github.com/openshift/origin/pkg/cmd/server/api" @@ -125,6 +126,7 @@ func NewCommandStartNetwork(basename string, out, errout io.Writer) (*cobra.Comm flags.StringVar(&options.ConfigFile, "config", "", "Location of the node configuration file to run from. When running from a configuration file, all other command-line arguments are ignored.") options.NodeArgs = NewDefaultNodeArgs() + options.NodeArgs.ListenArg.ListenAddr.DefaultPort = ports.ProxyHealthzPort options.NodeArgs.Components = NewNetworkComponentFlag() BindNodeNetworkArgs(options.NodeArgs, flags, "") BindImageFormatArgs(options.NodeArgs.ImageFormatArgs, flags, "") From ae05ccdfa65030511d21cfb4822ecba83d6bc9c0 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Sat, 30 Sep 2017 19:07:48 -0400 Subject: [PATCH 11/11] Ensure openshift start network can run in a pod Need to be able to take node-config from bootstrap node. For openshift start network the --kubeconfig flag from the CLI overrides the value of masterKubeConfig in the provided node config. If the value is empty (like it is by default) the in-cluster-config is used. Reorganize the node startup slightly so there is even less overlap between kubelet and network. A future change will completely separate these two initialization paths. --- contrib/completions/bash/openshift | 2 + contrib/completions/zsh/openshift | 2 + pkg/cmd/server/api/helpers.go | 21 +++++ pkg/cmd/server/api/validation/validation.go | 2 +- .../kubernetes/network/network_config.go | 12 +-- .../kubernetes/network/options/options.go | 89 +++++++++++++++++++ pkg/cmd/server/kubernetes/node/node_config.go | 3 +- .../server/kubernetes/node/options/options.go | 87 ++---------------- pkg/cmd/server/start/start_node.go | 47 +++++++--- 9 files changed, 166 insertions(+), 99 deletions(-) create mode 100644 pkg/cmd/server/kubernetes/network/options/options.go diff --git a/contrib/completions/bash/openshift b/contrib/completions/bash/openshift index 6363955429d8..8d4d344998ea 100644 --- a/contrib/completions/bash/openshift +++ b/contrib/completions/bash/openshift @@ -33708,6 +33708,8 @@ _openshift_start_network() local_nonpersistent_flags+=("--kubernetes=") flags+=("--latest-images") local_nonpersistent_flags+=("--latest-images") + flags+=("--listen=") + local_nonpersistent_flags+=("--listen=") flags+=("--network-plugin=") local_nonpersistent_flags+=("--network-plugin=") flags+=("--recursive-resolv-conf=") diff --git a/contrib/completions/zsh/openshift b/contrib/completions/zsh/openshift index efa981b7272a..c09ebc7ae163 100644 --- a/contrib/completions/zsh/openshift +++ b/contrib/completions/zsh/openshift @@ -33857,6 +33857,8 @@ _openshift_start_network() local_nonpersistent_flags+=("--kubernetes=") flags+=("--latest-images") local_nonpersistent_flags+=("--latest-images") + flags+=("--listen=") + local_nonpersistent_flags+=("--listen=") flags+=("--network-plugin=") local_nonpersistent_flags+=("--network-plugin=") flags+=("--recursive-resolv-conf=") diff --git a/pkg/cmd/server/api/helpers.go b/pkg/cmd/server/api/helpers.go index 3b37e587a10d..c19c69b8d706 100644 --- a/pkg/cmd/server/api/helpers.go +++ b/pkg/cmd/server/api/helpers.go @@ -334,6 +334,27 @@ func SetProtobufClientDefaults(overrides *ClientConnectionOverrides) { overrides.Burst *= 2 } +// GetKubeConfigOrInClusterConfig loads in-cluster config if kubeConfigFile is empty or the file if not, +// then applies overrides. +func GetKubeConfigOrInClusterConfig(kubeConfigFile string, overrides *ClientConnectionOverrides) (*restclient.Config, error) { + var kubeConfig *restclient.Config + var err error + if len(kubeConfigFile) == 0 { + kubeConfig, err = restclient.InClusterConfig() + } else { + loadingRules := &clientcmd.ClientConfigLoadingRules{} + loadingRules.ExplicitPath = kubeConfigFile + loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) + + kubeConfig, err = loader.ClientConfig() + } + if err != nil { + return nil, err + } + applyClientConnectionOverrides(overrides, kubeConfig) + return kubeConfig, nil +} + // TODO: clients should be copied and instantiated from a common client config, tweaked, then // given to individual controllers and other infrastructure components. func GetInternalKubeClient(kubeConfigFile string, overrides *ClientConnectionOverrides) (kclientsetinternal.Interface, *restclient.Config, error) { diff --git a/pkg/cmd/server/api/validation/validation.go b/pkg/cmd/server/api/validation/validation.go index 3999f6b562d6..15829806422d 100644 --- a/pkg/cmd/server/api/validation/validation.go +++ b/pkg/cmd/server/api/validation/validation.go @@ -128,7 +128,7 @@ func ValidateServingInfo(info api.ServingInfo, certificatesRequired bool, fldPat validationResults.AddErrors(ValidateFile(info.ClientCA, fldPath.Child("clientCA"))...) } } else { - if len(info.ClientCA) > 0 { + if certificatesRequired && len(info.ClientCA) > 0 { validationResults.AddErrors(field.Invalid(fldPath.Child("clientCA"), info.ClientCA, "cannot specify a clientCA without a certFile")) } } diff --git a/pkg/cmd/server/kubernetes/network/network_config.go b/pkg/cmd/server/kubernetes/network/network_config.go index 3c56730bd88f..e6fc4a277a5f 100644 --- a/pkg/cmd/server/kubernetes/network/network_config.go +++ b/pkg/cmd/server/kubernetes/network/network_config.go @@ -4,13 +4,12 @@ import ( "fmt" "net" - "github.com/golang/glog" - miekgdns "github.com/miekg/dns" kclientset "k8s.io/client-go/kubernetes" "k8s.io/kubernetes/pkg/apis/componentconfig" kclientsetexternal "k8s.io/kubernetes/pkg/client/clientset_generated/clientset" + kclientsetinternal "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" kinternalinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" configapi "github.com/openshift/origin/pkg/cmd/server/api" @@ -45,11 +44,15 @@ type NetworkConfig struct { // New creates a new network config object for running the networking components of the OpenShift node. func New(options configapi.NodeConfig, clusterDomain string, proxyConfig *componentconfig.KubeProxyConfiguration, enableProxy, enableDNS bool) (*NetworkConfig, error) { - internalKubeClient, kubeConfig, err := configapi.GetInternalKubeClient(options.MasterKubeConfig, options.MasterClientConnectionOverrides) + kubeConfig, err := configapi.GetKubeConfigOrInClusterConfig(options.MasterKubeConfig, options.MasterClientConnectionOverrides) + if err != nil { + return nil, err + } + internalKubeClient, err := kclientsetinternal.NewForConfig(kubeConfig) if err != nil { return nil, err } - externalKubeClient, _, err := configapi.GetExternalKubeClient(options.MasterKubeConfig, options.MasterClientConnectionOverrides) + externalKubeClient, err := kclientsetexternal.NewForConfig(kubeConfig) if err != nil { return nil, err } @@ -127,7 +130,6 @@ func New(options configapi.NodeConfig, clusterDomain string, proxyConfig *compon // TODO: use kubeletConfig.ResolverConfig as an argument to etcd in the event the // user sets it, instead of passing it to the kubelet. - glog.Infof("DNS Bind to %s", options.DNSBindAddress) config.DNSServer = dns.NewServer( dnsConfig, services, diff --git a/pkg/cmd/server/kubernetes/network/options/options.go b/pkg/cmd/server/kubernetes/network/options/options.go new file mode 100644 index 000000000000..1faafe41eca7 --- /dev/null +++ b/pkg/cmd/server/kubernetes/network/options/options.go @@ -0,0 +1,89 @@ +package node + +import ( + "fmt" + "net" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kerrors "k8s.io/apimachinery/pkg/util/errors" + kubeproxyoptions "k8s.io/kubernetes/cmd/kube-proxy/app" + "k8s.io/kubernetes/pkg/apis/componentconfig" + + configapi "github.com/openshift/origin/pkg/cmd/server/api" + cmdflags "github.com/openshift/origin/pkg/cmd/util/flags" +) + +// Build creates the network Kubernetes component configs for a given NodeConfig, or returns +// an error +func Build(options configapi.NodeConfig) (*componentconfig.KubeProxyConfiguration, error) { + proxyOptions, err := kubeproxyoptions.NewOptions() + if err != nil { + return nil, err + } + // get default config + proxyconfig := proxyOptions.GetConfig() + + proxyconfig.HostnameOverride = options.NodeName + + // BindAddress - Override default bind address from our config + addr := options.ServingInfo.BindAddress + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, fmt.Errorf("The provided value to bind to must be an ip:port %q", addr) + } + ip := net.ParseIP(host) + if ip == nil { + return nil, fmt.Errorf("The provided value to bind to must be an ip:port: %q", addr) + } + proxyconfig.BindAddress = ip.String() + // MetricsBindAddress - disable by default but allow enablement until we switch to + // reading proxy config directly + proxyconfig.MetricsBindAddress = "" + if arg := options.ProxyArguments["metrics-bind-address"]; len(arg) > 0 { + proxyconfig.MetricsBindAddress = arg[0] + } + delete(options.ProxyArguments, "metrics-bind-address") + + // OOMScoreAdj, ResourceContainer - clear, we don't run in a container + oomScoreAdj := int32(0) + proxyconfig.OOMScoreAdj = &oomScoreAdj + proxyconfig.ResourceContainer = "" + + // use the same client as the node + proxyconfig.ClientConnection.KubeConfigFile = options.MasterKubeConfig + + // ProxyMode, set to iptables + proxyconfig.Mode = "iptables" + + // IptablesSyncPeriod, set to our config value + syncPeriod, err := time.ParseDuration(options.IPTablesSyncPeriod) + if err != nil { + return nil, fmt.Errorf("Cannot parse the provided ip-tables sync period (%s) : %v", options.IPTablesSyncPeriod, err) + } + proxyconfig.IPTables.SyncPeriod = metav1.Duration{ + Duration: syncPeriod, + } + masqueradeBit := int32(0) + proxyconfig.IPTables.MasqueradeBit = &masqueradeBit + + // PortRange, use default + // HostnameOverride, use default + // ConfigSyncPeriod, use default + // MasqueradeAll, use default + // CleanupAndExit, use default + // KubeAPIQPS, use default, doesn't apply until we build a separate client + // KubeAPIBurst, use default, doesn't apply until we build a separate client + // UDPIdleTimeout, use default + + // Resolve cmd flags to add any user overrides + if err := cmdflags.Resolve(options.ProxyArguments, proxyOptions.AddFlags); len(err) > 0 { + return nil, kerrors.NewAggregate(err) + } + + if err := proxyOptions.Complete(); err != nil { + return nil, err + } + + return proxyconfig, nil +} diff --git a/pkg/cmd/server/kubernetes/node/node_config.go b/pkg/cmd/server/kubernetes/node/node_config.go index 5ed343f967c7..26dcb4df8e1f 100644 --- a/pkg/cmd/server/kubernetes/node/node_config.go +++ b/pkg/cmd/server/kubernetes/node/node_config.go @@ -12,6 +12,7 @@ import ( kubeletapp "k8s.io/kubernetes/cmd/kubelet/app" kubeletoptions "k8s.io/kubernetes/cmd/kubelet/app/options" "k8s.io/kubernetes/pkg/apis/componentconfig/v1alpha1" + kclientsetexternal "k8s.io/kubernetes/pkg/client/clientset_generated/clientset" "k8s.io/kubernetes/pkg/cloudprovider" "k8s.io/kubernetes/pkg/kubelet" dockertools "k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker" @@ -57,7 +58,7 @@ func New(options configapi.NodeConfig, server *kubeletoptions.KubeletServer) (*N return nil, err } // Make a separate client for event reporting, to avoid event QPS blocking node calls - eventClient, _, err := configapi.GetExternalKubeClient(options.MasterKubeConfig, options.MasterClientConnectionOverrides) + eventClient, err := kclientsetexternal.NewForConfig(kubeConfig) if err != nil { return nil, err } diff --git a/pkg/cmd/server/kubernetes/node/options/options.go b/pkg/cmd/server/kubernetes/node/options/options.go index f526ba3bf0f0..3a9794967fda 100644 --- a/pkg/cmd/server/kubernetes/node/options/options.go +++ b/pkg/cmd/server/kubernetes/node/options/options.go @@ -9,7 +9,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kerrors "k8s.io/apimachinery/pkg/util/errors" utilfeature "k8s.io/apiserver/pkg/util/feature" - kubeproxyoptions "k8s.io/kubernetes/cmd/kube-proxy/app" kubeletoptions "k8s.io/kubernetes/cmd/kubelet/app/options" "k8s.io/kubernetes/pkg/apis/componentconfig" "k8s.io/kubernetes/pkg/features" @@ -25,7 +24,7 @@ import ( // Build creates the core Kubernetes component configs for a given NodeConfig, or returns // an error -func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, *componentconfig.KubeProxyConfiguration, error) { +func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, error) { imageTemplate := variable.NewDefaultImageTemplate() imageTemplate.Format = options.ImageConfig.Format imageTemplate.Latest = options.ImageConfig.Latest @@ -39,11 +38,11 @@ func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, *compon kubeAddressStr, kubePortStr, err := net.SplitHostPort(options.ServingInfo.BindAddress) if err != nil { - return nil, nil, fmt.Errorf("cannot parse node address: %v", err) + return nil, fmt.Errorf("cannot parse node address: %v", err) } kubePort, err := strconv.Atoi(kubePortStr) if err != nil { - return nil, nil, fmt.Errorf("cannot parse node port: %v", err) + return nil, fmt.Errorf("cannot parse node port: %v", err) } // Defaults are tested in TestKubeletDefaults @@ -91,7 +90,7 @@ func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, *compon // Setup auth authnTTL, err := time.ParseDuration(options.AuthConfig.AuthenticationCacheTTL) if err != nil { - return nil, nil, err + return nil, err } server.Authentication = componentconfig.KubeletAuthentication{ X509: componentconfig.KubeletX509Authentication{ @@ -107,7 +106,7 @@ func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, *compon } authzTTL, err := time.ParseDuration(options.AuthConfig.AuthorizationCacheTTL) if err != nil { - return nil, nil, err + return nil, err } server.Authorization = componentconfig.KubeletAuthorization{ Mode: componentconfig.KubeletAuthorizationModeWebhook, @@ -121,13 +120,13 @@ func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, *compon // TODO: this should be done in config validation (along with the above) so we can provide // proper errors if err := cmdflags.Resolve(options.KubeletArguments, server.AddFlags); len(err) > 0 { - return nil, nil, kerrors.NewAggregate(err) + return nil, kerrors.NewAggregate(err) } // terminate early if feature gate is incorrect on the node if len(server.FeatureGates) > 0 { if err := utilfeature.DefaultFeatureGate.Set(server.FeatureGates); err != nil { - return nil, nil, err + return nil, err } } if utilfeature.DefaultFeatureGate.Enabled(features.RotateKubeletServerCertificate) { @@ -138,11 +137,6 @@ func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, *compon } } - proxyconfig, err := buildKubeProxyConfig(options) - if err != nil { - return nil, nil, err - } - if network.IsOpenShiftNetworkPlugin(options.NetworkConfig.NetworkPluginName) { // SDN plugin pod setup/teardown is implemented as a CNI plugin server.NetworkPluginName = kubeletcni.CNIPluginName @@ -152,72 +146,7 @@ func Build(options configapi.NodeConfig) (*kubeletoptions.KubeletServer, *compon server.HairpinMode = componentconfig.HairpinNone } - return server, proxyconfig, nil -} - -func buildKubeProxyConfig(options configapi.NodeConfig) (*componentconfig.KubeProxyConfiguration, error) { - proxyOptions, err := kubeproxyoptions.NewOptions() - if err != nil { - return nil, err - } - // get default config - proxyconfig := proxyOptions.GetConfig() - - // BindAddress - Override default bind address from our config - addr := options.ServingInfo.BindAddress - host, _, err := net.SplitHostPort(addr) - if err != nil { - return nil, fmt.Errorf("The provided value to bind to must be an ip:port %q", addr) - } - ip := net.ParseIP(host) - if ip == nil { - return nil, fmt.Errorf("The provided value to bind to must be an ip:port: %q", addr) - } - proxyconfig.BindAddress = ip.String() - // MetricsBindAddress - disable - proxyconfig.MetricsBindAddress = "" - - // OOMScoreAdj, ResourceContainer - clear, we don't run in a container - oomScoreAdj := int32(0) - proxyconfig.OOMScoreAdj = &oomScoreAdj - proxyconfig.ResourceContainer = "" - - // use the same client as the node - proxyconfig.ClientConnection.KubeConfigFile = options.MasterKubeConfig - - // ProxyMode, set to iptables - proxyconfig.Mode = "iptables" - - // IptablesSyncPeriod, set to our config value - syncPeriod, err := time.ParseDuration(options.IPTablesSyncPeriod) - if err != nil { - return nil, fmt.Errorf("Cannot parse the provided ip-tables sync period (%s) : %v", options.IPTablesSyncPeriod, err) - } - proxyconfig.IPTables.SyncPeriod = metav1.Duration{ - Duration: syncPeriod, - } - masqueradeBit := int32(0) - proxyconfig.IPTables.MasqueradeBit = &masqueradeBit - - // PortRange, use default - // HostnameOverride, use default - // ConfigSyncPeriod, use default - // MasqueradeAll, use default - // CleanupAndExit, use default - // KubeAPIQPS, use default, doesn't apply until we build a separate client - // KubeAPIBurst, use default, doesn't apply until we build a separate client - // UDPIdleTimeout, use default - - // Resolve cmd flags to add any user overrides - if err := cmdflags.Resolve(options.ProxyArguments, proxyOptions.AddFlags); len(err) > 0 { - return nil, kerrors.NewAggregate(err) - } - - if err := proxyOptions.Complete(); err != nil { - return nil, err - } - - return proxyconfig, nil + return server, nil } func ToFlags(config *kubeletoptions.KubeletServer) []string { diff --git a/pkg/cmd/server/start/start_node.go b/pkg/cmd/server/start/start_node.go index 4373818586e1..510881b38e16 100644 --- a/pkg/cmd/server/start/start_node.go +++ b/pkg/cmd/server/start/start_node.go @@ -29,6 +29,7 @@ import ( "github.com/openshift/origin/pkg/cmd/server/api/validation" "github.com/openshift/origin/pkg/cmd/server/crypto" "github.com/openshift/origin/pkg/cmd/server/kubernetes/network" + networkoptions "github.com/openshift/origin/pkg/cmd/server/kubernetes/network/options" "github.com/openshift/origin/pkg/cmd/server/kubernetes/node" nodeoptions "github.com/openshift/origin/pkg/cmd/server/kubernetes/node/options" cmdutil "github.com/openshift/origin/pkg/cmd/util" @@ -129,6 +130,7 @@ func NewCommandStartNetwork(basename string, out, errout io.Writer) (*cobra.Comm options.NodeArgs.ListenArg.ListenAddr.DefaultPort = ports.ProxyHealthzPort options.NodeArgs.Components = NewNetworkComponentFlag() BindNodeNetworkArgs(options.NodeArgs, flags, "") + BindListenArg(options.NodeArgs.ListenArg, flags, "") BindImageFormatArgs(options.NodeArgs.ImageFormatArgs, flags, "") BindKubeConnectionArgs(options.NodeArgs.KubeConnectionArgs, flags, "") @@ -219,7 +221,23 @@ func (o NodeOptions) RunNode() error { return err } - validationResults := validation.ValidateNodeConfig(nodeConfig, nil) + // allow listen address to be overriden + if addr := o.NodeArgs.ListenArg.ListenAddr; addr.Provided { + nodeConfig.ServingInfo.BindAddress = addr.HostPort(o.NodeArgs.ListenArg.ListenAddr.DefaultPort) + } + + var validationResults validation.ValidationResults + switch { + case o.NodeArgs.Components.Calculated().Equal(NewNetworkComponentFlag().Calculated()): + if len(nodeConfig.NodeName) == 0 { + nodeConfig.NodeName = o.NodeArgs.NodeName + } + nodeConfig.MasterKubeConfig = o.NodeArgs.KubeConnectionArgs.ClientConfigLoadingRules.ExplicitPath + validationResults = validation.ValidateInClusterNodeConfig(nodeConfig, nil) + default: + validationResults = validation.ValidateNodeConfig(nodeConfig, nil) + } + if len(validationResults.Warnings) != 0 { for _, warning := range validationResults.Warnings { glog.Warningf("Warning: %v, node start will continue.", warning) @@ -231,6 +249,7 @@ func (o NodeOptions) RunNode() error { } if err := ValidateRuntime(nodeConfig, o.NodeArgs.Components); err != nil { + glog.V(4).Infof("Unable to validate runtime configuration: %v", err) return err } @@ -412,8 +431,9 @@ func execKubelet(server *kubeletoptions.KubeletServer) (bool, error) { } func StartNode(nodeConfig configapi.NodeConfig, components *utilflags.ComponentFlag) error { - server, proxyConfig, err := nodeoptions.Build(nodeConfig) + server, err := nodeoptions.Build(nodeConfig) if err != nil { + glog.V(4).Infof("Unable to build node options: %v", err) return err } @@ -429,33 +449,34 @@ func StartNode(nodeConfig configapi.NodeConfig, components *utilflags.ComponentF } } - networkConfig, err := network.New(nodeConfig, server.ClusterDomain, proxyConfig, components.Enabled(ComponentProxy), components.Enabled(ComponentDNS) && len(nodeConfig.DNSBindAddress) > 0) + proxyConfig, err := networkoptions.Build(nodeConfig) if err != nil { + glog.V(4).Infof("Unable to build network options: %v", err) return err } - - config, err := node.New(nodeConfig, server) + networkConfig, err := network.New(nodeConfig, server.ClusterDomain, proxyConfig, components.Enabled(ComponentProxy), components.Enabled(ComponentDNS) && len(nodeConfig.DNSBindAddress) > 0) if err != nil { + glog.V(4).Infof("Unable to initialize network configuration: %v", err) return err } if components.Enabled(ComponentKubelet) { + config, err := node.New(nodeConfig, server) + if err != nil { + glog.V(4).Infof("Unable to create node configuration: %v", err) + return err + } glog.Infof("Starting node %s (%s)", config.KubeletServer.HostnameOverride, version.Get().String()) - } else { - glog.Infof("Starting node networking %s (%s)", config.KubeletServer.HostnameOverride, version.Get().String()) - } - // preconditions - if components.Enabled(ComponentKubelet) { config.EnsureKubeletAccess() config.EnsureVolumeDir() config.EnsureDocker(docker.NewHelper()) config.EnsureLocalQuota(nodeConfig) // must be performed after EnsureVolumeDir - } - - if components.Enabled(ComponentKubelet) { config.RunKubelet() + } else { + glog.Infof("Starting node networking %s (%s)", nodeConfig.NodeName, version.Get().String()) } + if components.Enabled(ComponentPlugins) { networkConfig.RunSDN() }