Adding support for custom TLS ciphers in api server and kubelet

pull/6/head
Victor Garcia 2017-07-12 23:49:41 -07:00
parent 5636634879
commit d7dbc96c70
16 changed files with 444 additions and 1 deletions

View File

@ -442,6 +442,10 @@ func AddKubeletConfigFlags(fs *pflag.FlagSet, c *kubeletconfig.KubeletConfigurat
"If --tls-cert-file and --tls-private-key-file are not provided, a self-signed certificate and key "+
"are generated for the public address and saved to the directory passed to --cert-dir.")
fs.StringVar(&c.TLSPrivateKeyFile, "tls-private-key-file", c.TLSPrivateKeyFile, "File containing x509 private key matching --tls-cert-file.")
fs.StringSliceVar(&c.TLSCipherSuites, "tls-cipher-suites", c.TLSCipherSuites,
"Comma-separated list of cipher suites for the server. "+
"Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). "+
"If omitted, the default Go cipher suites will be used")
fs.Int32Var(&c.RegistryPullQPS, "registry-qps", c.RegistryPullQPS, "If > 0, limit registry pull QPS to this value. If 0, unlimited.")
fs.Int32Var(&c.RegistryBurst, "registry-burst", c.RegistryBurst, "Maximum size of a bursty pulls, temporarily allows pulls to burst to this number, while still not exceeding registry-qps. Only used if --registry-qps > 0")

View File

@ -567,12 +567,19 @@ func InitializeTLS(kf *options.KubeletFlags, kc *kubeletconfiginternal.KubeletCo
glog.V(4).Infof("Using self-signed cert (%s, %s)", kc.TLSCertFile, kc.TLSPrivateKeyFile)
}
}
tlsCipherSuites, err := flag.TLSCipherSuites(kc.TLSCipherSuites)
if err != nil {
return nil, err
}
tlsOptions := &server.TLSOptions{
Config: &tls.Config{
// Can't use SSLv3 because of POODLE and BEAST
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS12,
CipherSuites: tlsCipherSuites,
},
CertFile: kc.TLSCertFile,
KeyFile: kc.TLSPrivateKeyFile,

View File

@ -175,6 +175,7 @@ var (
"HairpinMode",
"HealthzBindAddress",
"HealthzPort",
"TLSCipherSuites[*]",
"IPTablesDropBit",
"IPTablesMasqueradeBit",
"ImageGCHighThresholdPercent",

View File

@ -85,6 +85,9 @@ type KubeletConfiguration struct {
// tlsPrivateKeyFile is the ile containing x509 private key matching
// tlsCertFile.
TLSPrivateKeyFile string
// TLSCipherSuites is the list of allowed cipher suites for the server.
// Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants).
TLSCipherSuites []string
// authentication specifies how requests to the Kubelet's server are authenticated
Authentication KubeletAuthentication
// authorization specifies how requests to the Kubelet's server are authorized

View File

@ -85,6 +85,9 @@ type KubeletConfiguration struct {
// tlsPrivateKeyFile is the ile containing x509 private key matching
// tlsCertFile.
TLSPrivateKeyFile string `json:"tlsPrivateKeyFile"`
// TLSCipherSuites is the list of allowed cipher suites for the server.
// Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants).
TLSCipherSuites []string `json:"tlsCipherSuites"`
// authentication specifies how requests to the Kubelet's server are authenticated
Authentication KubeletAuthentication `json:"authentication"`
// authorization specifies how requests to the Kubelet's server are authorized

View File

@ -158,6 +158,7 @@ func autoConvert_v1alpha1_KubeletConfiguration_To_kubeletconfig_KubeletConfigura
}
out.TLSCertFile = in.TLSCertFile
out.TLSPrivateKeyFile = in.TLSPrivateKeyFile
out.TLSCipherSuites = *(*[]string)(unsafe.Pointer(&in.TLSCipherSuites))
if err := Convert_v1alpha1_KubeletAuthentication_To_kubeletconfig_KubeletAuthentication(&in.Authentication, &out.Authentication, s); err != nil {
return err
}
@ -279,6 +280,7 @@ func autoConvert_kubeletconfig_KubeletConfiguration_To_v1alpha1_KubeletConfigura
}
out.TLSCertFile = in.TLSCertFile
out.TLSPrivateKeyFile = in.TLSPrivateKeyFile
out.TLSCipherSuites = *(*[]string)(unsafe.Pointer(&in.TLSCipherSuites))
if err := Convert_kubeletconfig_KubeletAuthentication_To_v1alpha1_KubeletAuthentication(&in.Authentication, &out.Authentication, s); err != nil {
return err
}

View File

@ -132,6 +132,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) {
**out = **in
}
}
if in.TLSCipherSuites != nil {
in, out := &in.TLSCipherSuites, &out.TLSCipherSuites
*out = make([]string, len(*in))
copy(*out, *in)
}
in.Authentication.DeepCopyInto(&out.Authentication)
out.Authorization = in.Authorization
if in.RegistryPullQPS != nil {

View File

@ -105,6 +105,11 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) {
}
}
}
if in.TLSCipherSuites != nil {
in, out := &in.TLSCipherSuites, &out.TLSCipherSuites
*out = make([]string, len(*in))
copy(*out, *in)
}
out.Authentication = in.Authentication
out.Authorization = in.Authorization
if in.ClusterDNS != nil {

View File

@ -51,6 +51,9 @@ type SecureServingOptions struct {
ServerCert GeneratableKeyCert
// SNICertKeys are named CertKeys for serving secure traffic with SNI support.
SNICertKeys []utilflag.NamedCertKey
// CipherSuites is the list of allowed cipher suites for the server.
// Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants).
CipherSuites []string
}
type CertKey struct {
@ -134,6 +137,11 @@ func (s *SecureServingOptions) AddFlags(fs *pflag.FlagSet) {
"Controllers. This must be a valid PEM-encoded CA bundle. Altneratively, the certificate authority "+
"can be appended to the certificate provided by --tls-cert-file.")
fs.StringSliceVar(&s.CipherSuites, "tls-cipher-suites", s.CipherSuites,
"Comma-separated list of cipher suites for the server. "+
"Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). "+
"If omitted, the default Go cipher suites will be used")
fs.Var(utilflag.NewNamedCertKeyArray(&s.SNICertKeys), "tls-sni-cert-key", ""+
"A pair of x509 certificate and private key file paths, optionally suffixed with a list of "+
"domain patterns which are fully qualified domain names, possibly with prefixed wildcard "+
@ -233,6 +241,14 @@ func (s *SecureServingOptions) applyServingInfoTo(c *server.Config) error {
}
}
if len(s.CipherSuites) != 0 {
cipherSuites, err := utilflag.TLSCipherSuites(s.CipherSuites)
if err != nil {
return err
}
secureServingInfo.CipherSuites = cipherSuites
}
// load SNI certs
namedTLSCerts := make([]server.NamedTLSCert, 0, len(s.SNICertKeys))
for _, nck := range s.SNICertKeys {

View File

@ -9,6 +9,7 @@ load(
go_test(
name = "go_default_test",
srcs = [
"ciphersuites_flag_test.go",
"colon_separated_multimap_string_string_test.go",
"langle_separated_map_string_string_test.go",
"map_string_bool_test.go",
@ -23,6 +24,7 @@ go_test(
go_library(
name = "go_default_library",
srcs = [
"ciphersuites_flag.go",
"colon_separated_multimap_string_string.go",
"configuration_map.go",
"flags.go",

View File

@ -0,0 +1,64 @@
/*
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 flag
import (
"crypto/tls"
"fmt"
)
// ciphers maps strings into tls package cipher constants in
// https://golang.org/pkg/crypto/tls/#pkg-constants
var ciphers = map[string]uint16{
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
}
func TLSCipherSuites(cipherNames []string) ([]uint16, error) {
if len(cipherNames) == 0 {
return nil, nil
}
ciphersIntSlice := make([]uint16, 0)
for _, cipher := range cipherNames {
intValue, ok := ciphers[cipher]
if !ok {
return nil, fmt.Errorf("Cipher suite %s not supported or doesn't exist", cipher)
}
ciphersIntSlice = append(ciphersIntSlice, intValue)
}
return ciphersIntSlice, nil
}

View File

@ -0,0 +1,100 @@
/*
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 flag
import (
"crypto/tls"
"fmt"
"go/importer"
"reflect"
"strings"
"testing"
)
func TestStrToUInt16(t *testing.T) {
tests := []struct {
flag []string
expected []uint16
expected_error bool
}{
{
// Happy case
flag: []string{"TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"},
expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA},
expected_error: false,
},
{
// One flag only
flag: []string{"TLS_RSA_WITH_RC4_128_SHA"},
expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA},
expected_error: false,
},
{
// Empty flag
flag: []string{},
expected: nil,
expected_error: false,
},
{
// Duplicated flag
flag: []string{"TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_RC4_128_SHA"},
expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_RC4_128_SHA},
expected_error: false,
},
{
// Invalid flag
flag: []string{"foo"},
expected: nil,
expected_error: true,
},
}
for i, test := range tests {
uIntFlags, err := TLSCipherSuites(test.flag)
if reflect.DeepEqual(uIntFlags, test.expected) == false {
t.Errorf("%d: expected %+v, got %+v", i, test.expected, uIntFlags)
}
if test.expected_error && err == nil {
t.Errorf("%d: expecting error, got %+v", i, err)
}
}
}
func TestConstantMaps(t *testing.T) {
pkg, err := importer.Default().Import("crypto/tls")
if err != nil {
fmt.Printf("error: %s\n", err.Error())
return
}
discoveredCiphers := map[string]bool{}
for _, declName := range pkg.Scope().Names() {
if strings.HasPrefix(declName, "TLS_RSA_") || strings.HasPrefix(declName, "TLS_ECDHE_") {
discoveredCiphers[declName] = true
}
}
for k := range discoveredCiphers {
if _, ok := ciphers[k]; !ok {
t.Errorf("discovered cipher tls.%s not in ciphers map", k)
}
}
for k := range ciphers {
if _, ok := discoveredCiphers[k]; !ok {
t.Errorf("ciphers map has %s not in tls package", k)
}
}
}

View File

@ -58,6 +58,7 @@ filegroup(
"//test/integration/secrets:all-srcs",
"//test/integration/serviceaccount:all-srcs",
"//test/integration/storageclasses:all-srcs",
"//test/integration/tls:all-srcs",
"//test/integration/ttlcontroller:all-srcs",
"//test/integration/volume:all-srcs",
],

View File

@ -0,0 +1,35 @@
load("@io_bazel_rules_go//go:def.bzl", "go_test")
go_test(
name = "go_default_test",
size = "large",
srcs = [
"ciphers_test.go",
"main_test.go",
],
importpath = "k8s.io/kubernetes/test/integration/tls",
tags = ["integration"],
deps = [
"//cmd/kube-apiserver/app:go_default_library",
"//cmd/kube-apiserver/app/options:go_default_library",
"//test/integration/framework:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -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 tls
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"sync/atomic"
"testing"
"time"
"k8s.io/apimachinery/pkg/util/wait"
genericapiserver "k8s.io/apiserver/pkg/server"
client "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/kubernetes/cmd/kube-apiserver/app"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/test/integration/framework"
)
func runBasicSecureAPIServer(t *testing.T, ciphers []string) (uint32, error) {
certDir, _ := ioutil.TempDir("", "test-integration-tls")
defer os.RemoveAll(certDir)
_, defaultServiceClusterIPRange, _ := net.ParseCIDR("10.0.0.0/24")
kubeClientConfigValue := atomic.Value{}
var kubePort uint32
go func() {
// always get a fresh port in case something claimed the old one
freePort, err := framework.FindFreeLocalPort()
if err != nil {
t.Fatal(err)
}
atomic.StoreUint32(&kubePort, uint32(freePort))
kubeAPIServerOptions := options.NewServerRunOptions()
kubeAPIServerOptions.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
kubeAPIServerOptions.SecureServing.BindPort = freePort
kubeAPIServerOptions.SecureServing.ServerCert.CertDirectory = certDir
kubeAPIServerOptions.SecureServing.CipherSuites = ciphers
kubeAPIServerOptions.InsecureServing.BindPort = 0
kubeAPIServerOptions.Etcd.StorageConfig.ServerList = []string{framework.GetEtcdURL()}
kubeAPIServerOptions.ServiceClusterIPRange = *defaultServiceClusterIPRange
tunneler, proxyTransport, err := app.CreateNodeDialer(kubeAPIServerOptions)
if err != nil {
t.Fatal(err)
}
kubeAPIServerConfig, sharedInformers, versionedInformers, _, _, err := app.CreateKubeAPIServerConfig(kubeAPIServerOptions, tunneler, proxyTransport)
if err != nil {
t.Fatal(err)
}
kubeAPIServerConfig.ExtraConfig.EnableCoreControllers = false
kubeClientConfigValue.Store(kubeAPIServerConfig.GenericConfig.LoopbackClientConfig)
kubeAPIServer, err := app.CreateKubeAPIServer(kubeAPIServerConfig, genericapiserver.EmptyDelegate, sharedInformers, versionedInformers)
if err != nil {
t.Fatal(err)
}
if err := kubeAPIServer.GenericAPIServer.PrepareRun().Run(wait.NeverStop); err != nil {
t.Log(err)
}
time.Sleep(100 * time.Millisecond)
}()
// Ensure server is ready
err := wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) {
obj := kubeClientConfigValue.Load()
if obj == nil {
return false, nil
}
kubeClientConfig := kubeClientConfigValue.Load().(*rest.Config)
kubeClientConfig.ContentType = ""
kubeClientConfig.AcceptContentTypes = ""
kubeClient, err := client.NewForConfig(kubeClientConfig)
if err != nil {
// this happens because we race the API server start
t.Log(err)
return false, nil
}
if _, err := kubeClient.Discovery().ServerVersion(); err != nil {
return false, nil
}
return true, nil
})
if err != nil {
return 0, err
}
securePort := atomic.LoadUint32(&kubePort)
return securePort, nil
}
func TestAPICiphers(t *testing.T) {
basicServerCiphers := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"}
kubePort, err := runBasicSecureAPIServer(t, basicServerCiphers)
if err != nil {
t.Fatal(err)
}
tests := []struct {
clientCiphers []uint16
expectedError bool
}{
{
// Not supported cipher
clientCiphers: []uint16{tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA},
expectedError: true,
},
{
// Supported cipher
clientCiphers: []uint16{tls.TLS_RSA_WITH_AES_256_CBC_SHA},
expectedError: false,
},
}
for i, test := range tests {
runTestAPICiphers(t, i, kubePort, test.clientCiphers, test.expectedError)
}
}
func runTestAPICiphers(t *testing.T, testID int, kubePort uint32, clientCiphers []uint16, expectedError bool) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
CipherSuites: clientCiphers,
},
}
client := &http.Client{Transport: tr}
req, err := http.NewRequest("GET", fmt.Sprintf("https://127.0.0.1:%d", kubePort), nil)
if err != nil {
t.Fatal(err)
}
resp, err := client.Do(req)
if expectedError == true && err == nil {
t.Fatalf("%d: expecting error for cipher test, client cipher is supported and it should't", testID)
} else if err != nil && expectedError == false {
t.Fatalf("%d: not expecting error by client with cipher failed: %+v", testID, err)
}
if err == nil {
defer resp.Body.Close()
}
}

View File

@ -0,0 +1,27 @@
/*
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 tls
import (
"testing"
"k8s.io/kubernetes/test/integration/framework"
)
func TestMain(m *testing.M) {
framework.EtcdMain(m.Run)
}