diff --git a/federation/pkg/kubefed/init/BUILD b/federation/pkg/kubefed/init/BUILD index 3859a5fdb02..e515acbb949 100644 --- a/federation/pkg/kubefed/init/BUILD +++ b/federation/pkg/kubefed/init/BUILD @@ -21,12 +21,15 @@ go_library( "//pkg/api/resource:go_default_library", "//pkg/apis/extensions:go_default_library", "//pkg/client/clientset_generated/internalclientset:go_default_library", + "//pkg/client/unversioned/clientcmd:go_default_library", + "//pkg/client/unversioned/clientcmd/api:go_default_library", "//pkg/kubectl/cmd/templates:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", "//pkg/util/cert:go_default_library", "//pkg/util/cert/triple:go_default_library", "//pkg/util/intstr:go_default_library", "//pkg/util/wait:go_default_library", + "//pkg/version:go_default_library", "//vendor:github.com/spf13/cobra", ], ) @@ -36,5 +39,22 @@ go_test( srcs = ["init_test.go"], library = "go_default_library", tags = ["automanaged"], - deps = [], + deps = [ + "//federation/pkg/kubefed/testing:go_default_library", + "//federation/pkg/kubefed/util:go_default_library", + "//pkg/api:go_default_library", + "//pkg/api/errors:go_default_library", + "//pkg/api/resource:go_default_library", + "//pkg/api/testapi:go_default_library", + "//pkg/api/unversioned:go_default_library", + "//pkg/api/v1:go_default_library", + "//pkg/apis/extensions/v1beta1:go_default_library", + "//pkg/client/restclient/fake:go_default_library", + "//pkg/client/typed/dynamic:go_default_library", + "//pkg/client/unversioned/clientcmd:go_default_library", + "//pkg/kubectl/cmd/testing:go_default_library", + "//pkg/kubectl/cmd/util:go_default_library", + "//pkg/util/intstr:go_default_library", + "//vendor:k8s.io/client-go/pkg/util/diff", + ], ) diff --git a/federation/pkg/kubefed/init/init.go b/federation/pkg/kubefed/init/init.go index ad2a8475fd2..46318d491e3 100644 --- a/federation/pkg/kubefed/init/init.go +++ b/federation/pkg/kubefed/init/init.go @@ -42,12 +42,15 @@ import ( "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/apis/extensions" client "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" + clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" certutil "k8s.io/kubernetes/pkg/util/cert" triple "k8s.io/kubernetes/pkg/util/cert/triple" "k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/wait" + "k8s.io/kubernetes/pkg/version" "github.com/spf13/cobra" ) @@ -55,6 +58,7 @@ import ( const ( APIServerCN = "federation-apiserver" ControllerManagerCN = "federation-controller-manager" + AdminCN = "admin" HostClusterLocalDNSZoneName = "cluster.local." lbAddrRetryInterval = 5 * time.Second @@ -92,7 +96,7 @@ var ( "module": "federation-controller-manager", } - hyperkubeImage = "gcr.io/google_containers/hyperkube-amd64:v1.5.0" + hyperkubeImageName = "gcr.io/google_containers/hyperkube-amd64" ) // NewCmdInit defines the `init` command that bootstraps a federation @@ -109,8 +113,11 @@ func NewCmdInit(cmdOut io.Writer, config util.AdminConfig) *cobra.Command { }, } + defaultImage := fmt.Sprintf("%s:%s", hyperkubeImageName, version.Get()) + util.AddSubcommandFlags(cmd) cmd.Flags().String("dns-zone-name", "", "DNS suffix for this federation. Federated Service DNS names are published with this suffix.") + cmd.Flags().String("image", defaultImage, "Image to use for federation API server and controller manager binaries.") return cmd } @@ -118,6 +125,7 @@ type entityKeyPairs struct { ca *triple.KeyPair server *triple.KeyPair controllerManager *triple.KeyPair + admin *triple.KeyPair } // initFederation initializes a federation control plane. @@ -129,6 +137,7 @@ func initFederation(cmdOut io.Writer, config util.AdminConfig, cmd *cobra.Comman return err } dnsZoneName := cmdutil.GetFlagString(cmd, "dns-zone-name") + image := cmdutil.GetFlagString(cmd, "image") hostFactory := config.HostFactory(initFlags.Host, initFlags.Kubeconfig) hostClientset, err := hostFactory.ClientSet() @@ -189,14 +198,26 @@ func initFederation(cmdOut io.Writer, config util.AdminConfig, cmd *cobra.Comman advertiseAddress = ips[0] } + endpoint := advertiseAddress + if advertiseAddress == "" && len(hostnames) > 0 { + endpoint = hostnames[0] + } + // 6. Create federation API server - _, err = createAPIServer(hostClientset, initFlags.FederationSystemNamespace, serverName, serverCredName, pvc.Name, advertiseAddress) + _, err = createAPIServer(hostClientset, initFlags.FederationSystemNamespace, serverName, image, serverCredName, pvc.Name, advertiseAddress) if err != nil { return err } // 7. Create federation controller manager - _, err = createControllerManager(hostClientset, initFlags.FederationSystemNamespace, initFlags.Name, cmName, cmKubeconfigName, dnsZoneName) + _, err = createControllerManager(hostClientset, initFlags.FederationSystemNamespace, initFlags.Name, cmName, image, cmKubeconfigName, dnsZoneName) + if err != nil { + return err + } + + // 8. Write the federation API server endpoint info, credentials + // and context to kubeconfig + err = updateKubeconfig(config, initFlags.Name, endpoint, entKeyPairs) if err != nil { return err } @@ -282,10 +303,15 @@ func genCerts(svcNamespace, name, svcName, localDNSZoneName string, ips, hostnam if err != nil { return nil, fmt.Errorf("failed to create federation controller manager client key and certificate: %v", err) } + admin, err := triple.NewClientKeyPair(ca, AdminCN) + if err != nil { + return nil, fmt.Errorf("failed to create client key and certificate for an admin: %v", err) + } return &entityKeyPairs{ ca: ca, server: server, controllerManager: cm, + admin: admin, }, nil } @@ -305,7 +331,6 @@ func createAPIServerCredentialsSecret(clientset *client.Clientset, namespace, cr // Boilerplate to create the secret in the host cluster. return clientset.Core().Secrets(namespace).Create(secret) - } func createControllerManagerKubeconfigSecret(clientset *client.Clientset, namespace, name, svcName, kubeconfigName string, entKeyPairs *entityKeyPairs) (*api.Secret, error) { @@ -356,7 +381,7 @@ func createPVC(clientset *client.Clientset, namespace, svcName string) (*api.Per return clientset.Core().PersistentVolumeClaims(namespace).Create(pvc) } -func createAPIServer(clientset *client.Clientset, namespace, name, credentialsName, pvcName, advertiseAddress string) (*extensions.Deployment, error) { +func createAPIServer(clientset *client.Clientset, namespace, name, image, credentialsName, pvcName, advertiseAddress string) (*extensions.Deployment, error) { command := []string{ "/hyperkube", "federation-apiserver", @@ -364,8 +389,6 @@ func createAPIServer(clientset *client.Clientset, namespace, name, credentialsNa "--etcd-servers=http://localhost:2379", "--service-cluster-ip-range=10.0.0.0/16", "--secure-port=443", - "--token-auth-file=/etc/federation/apiserver/known_tokens.csv", - "--basic-auth-file=/etc/federation/apiserver/basic_auth.csv", "--client-ca-file=/etc/federation/apiserver/ca.crt", "--tls-cert-file=/etc/federation/apiserver/server.crt", "--tls-private-key-file=/etc/federation/apiserver/server.key", @@ -394,7 +417,7 @@ func createAPIServer(clientset *client.Clientset, namespace, name, credentialsNa Containers: []api.Container{ { Name: "apiserver", - Image: hyperkubeImage, + Image: image, Command: command, Ports: []api.ContainerPort{ { @@ -456,7 +479,7 @@ func createAPIServer(clientset *client.Clientset, namespace, name, credentialsNa return clientset.Extensions().Deployments(namespace).Create(dep) } -func createControllerManager(clientset *client.Clientset, namespace, name, cmName, kubeconfigName, dnsZoneName string) (*extensions.Deployment, error) { +func createControllerManager(clientset *client.Clientset, namespace, name, cmName, image, kubeconfigName, dnsZoneName string) (*extensions.Deployment, error) { dep := &extensions.Deployment{ ObjectMeta: api.ObjectMeta{ Name: cmName, @@ -474,7 +497,7 @@ func createControllerManager(clientset *client.Clientset, namespace, name, cmNam Containers: []api.Container{ { Name: "controller-manager", - Image: hyperkubeImage, + Image: image, Command: []string{ "/hyperkube", "federation-controller-manager", @@ -527,3 +550,44 @@ func printSuccess(cmdOut io.Writer, ips, hostnames []string) error { _, err := fmt.Fprintf(cmdOut, "Federation API server is running at: %s\n", strings.Join(svcEndpoints, ", ")) return err } + +func updateKubeconfig(config util.AdminConfig, name, endpoint string, entKeyPairs *entityKeyPairs) error { + po := config.PathOptions() + kubeconfig, err := po.GetStartingConfig() + if err != nil { + return err + } + + // Populate API server endpoint info. + cluster := clientcmdapi.NewCluster() + // Prefix "https" as the URL scheme to endpoint. + if !strings.HasPrefix(endpoint, "https://") { + endpoint = fmt.Sprintf("https://%s", endpoint) + } + cluster.Server = endpoint + cluster.CertificateAuthorityData = certutil.EncodeCertPEM(entKeyPairs.ca.Cert) + + // Populate credentials. + authInfo := clientcmdapi.NewAuthInfo() + authInfo.ClientCertificateData = certutil.EncodeCertPEM(entKeyPairs.admin.Cert) + authInfo.ClientKeyData = certutil.EncodePrivateKeyPEM(entKeyPairs.admin.Key) + authInfo.Username = AdminCN + + // Populate context. + context := clientcmdapi.NewContext() + context.Cluster = name + context.AuthInfo = name + + // Update the config struct with API server endpoint info, + // credentials and context. + kubeconfig.Clusters[name] = cluster + kubeconfig.AuthInfos[name] = authInfo + kubeconfig.Contexts[name] = context + + // Write the update kubeconfig. + if err := clientcmd.ModifyConfig(po, *kubeconfig, true); err != nil { + return err + } + + return nil +} diff --git a/federation/pkg/kubefed/init/init_test.go b/federation/pkg/kubefed/init/init_test.go index 14de4c636de..87d4b31f9ca 100644 --- a/federation/pkg/kubefed/init/init_test.go +++ b/federation/pkg/kubefed/init/init_test.go @@ -17,6 +17,7 @@ limitations under the License. package init import ( + "bytes" "crypto/tls" "crypto/x509" "fmt" @@ -28,6 +29,23 @@ import ( "strings" "testing" "time" + + "k8s.io/client-go/pkg/util/diff" + kubefedtesting "k8s.io/kubernetes/federation/pkg/kubefed/testing" + "k8s.io/kubernetes/federation/pkg/kubefed/util" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/resource" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" + "k8s.io/kubernetes/pkg/client/restclient/fake" + "k8s.io/kubernetes/pkg/client/typed/dynamic" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" + cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/util/intstr" ) const ( @@ -38,15 +56,79 @@ const ( helloMsg = "Hello, certificate test!" ) -type clientServerTLSConfigs struct { - server *tls.Config - client *tls.Config -} +func TestInitFederation(t *testing.T) { + cmdErrMsg := "" + cmdutil.BehaviorOnFatal(func(str string, code int) { + cmdErrMsg = str + }) -type certParams struct { - cAddr string - ips []string - hostnames []string + fakeKubeFiles, err := kubefedtesting.FakeKubeconfigFiles() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer kubefedtesting.RemoveFakeKubeconfigFiles(fakeKubeFiles) + + testCases := []struct { + federation string + kubeconfigGlobal string + kubeconfigExplicit string + dnsZoneName string + lbIP string + image string + expectedErr string + }{ + { + federation: "union", + kubeconfigGlobal: fakeKubeFiles[0], + kubeconfigExplicit: "", + dnsZoneName: "example.test.", + lbIP: "10.20.30.40", + image: "example.test/foo:bar", + expectedErr: "", + }, + } + + for i, tc := range testCases { + cmdErrMsg = "" + buf := bytes.NewBuffer([]byte{}) + + hostFactory, err := fakeInitHostFactory(tc.federation, util.DefaultFederationSystemNamespace, tc.lbIP, tc.dnsZoneName, tc.image) + if err != nil { + t.Fatalf("[%d] unexpected error: %v", i, err) + } + + adminConfig, err := kubefedtesting.NewFakeAdminConfig(hostFactory, tc.kubeconfigGlobal) + if err != nil { + t.Fatalf("[%d] unexpected error: %v", i, err) + } + + cmd := NewCmdInit(buf, adminConfig) + + cmd.Flags().Set("kubeconfig", tc.kubeconfigExplicit) + cmd.Flags().Set("host-cluster-context", "substrate") + cmd.Flags().Set("dns-zone-name", tc.dnsZoneName) + cmd.Flags().Set("image", tc.image) + cmd.Run(cmd, []string{tc.federation}) + + if tc.expectedErr == "" { + // uses the name from the federation, not the response + // Actual data passed are tested in the fake secret and cluster + // REST clients. + want := fmt.Sprintf("Federation API server is running at: %s\n", tc.lbIP) + if got := buf.String(); got != want { + t.Errorf("[%d] unexpected output: got: %s, want: %s", i, got, want) + if cmdErrMsg != "" { + t.Errorf("[%d] unexpected error message: %s", i, cmdErrMsg) + } + } + } else { + if cmdErrMsg != tc.expectedErr { + t.Errorf("[%d] expected error: %s, got: %s, output: %s", i, tc.expectedErr, cmdErrMsg, buf.String()) + } + } + + testKubeconfigUpdate(t, tc.federation, tc.lbIP, tc.kubeconfigGlobal, tc.kubeconfigExplicit) + } } // TestCertsTLS tests TLS handshake with client authentication for any server @@ -288,6 +370,446 @@ func TestCertsHTTPS(t *testing.T) { } } +func fakeInitHostFactory(federationName, namespaceName, ip, dnsZoneName, image string) (cmdutil.Factory, error) { + svcName := federationName + "-apiserver" + svcUrlPrefix := "/api/v1/namespaces/federation-system/services" + credSecretName := svcName + "-credentials" + cmKubeconfigSecretName := federationName + "-controller-manager-kubeconfig" + capacity, err := resource.ParseQuantity("10Gi") + if err != nil { + return nil, err + } + pvcName := svcName + "-etcd-claim" + replicas := int32(1) + + namespace := v1.Namespace{ + TypeMeta: unversioned.TypeMeta{ + Kind: "Namespace", + APIVersion: testapi.Default.GroupVersion().String(), + }, + ObjectMeta: v1.ObjectMeta{ + Name: namespaceName, + }, + } + + svc := v1.Service{ + TypeMeta: unversioned.TypeMeta{ + Kind: "Service", + APIVersion: testapi.Default.GroupVersion().String(), + }, + ObjectMeta: v1.ObjectMeta{ + Namespace: namespaceName, + Name: svcName, + Labels: componentLabel, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeLoadBalancer, + Selector: apiserverSvcSelector, + Ports: []v1.ServicePort{ + { + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(443), + }, + }, + }, + } + + svcWithLB := svc + svcWithLB.Status = v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + { + IP: ip, + }, + }, + }, + } + + credSecret := v1.Secret{ + TypeMeta: unversioned.TypeMeta{ + Kind: "Secret", + APIVersion: testapi.Default.GroupVersion().String(), + }, + ObjectMeta: v1.ObjectMeta{ + Name: credSecretName, + Namespace: namespaceName, + }, + Data: nil, + } + + cmKubeconfigSecret := v1.Secret{ + TypeMeta: unversioned.TypeMeta{ + Kind: "Secret", + APIVersion: testapi.Default.GroupVersion().String(), + }, + ObjectMeta: v1.ObjectMeta{ + Name: cmKubeconfigSecretName, + Namespace: namespaceName, + }, + Data: nil, + } + + pvc := v1.PersistentVolumeClaim{ + TypeMeta: unversioned.TypeMeta{ + Kind: "PersistentVolumeClaim", + APIVersion: testapi.Default.GroupVersion().String(), + }, + ObjectMeta: v1.ObjectMeta{ + Name: pvcName, + Namespace: namespaceName, + Labels: componentLabel, + Annotations: map[string]string{ + "volume.alpha.kubernetes.io/storage-class": "yes", + }, + }, + Spec: v1.PersistentVolumeClaimSpec{ + AccessModes: []v1.PersistentVolumeAccessMode{ + v1.ReadWriteOnce, + }, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceStorage: capacity, + }, + }, + }, + } + + apiserver := v1beta1.Deployment{ + TypeMeta: unversioned.TypeMeta{ + Kind: "Deployment", + APIVersion: testapi.Extensions.GroupVersion().String(), + }, + ObjectMeta: v1.ObjectMeta{ + Name: svcName, + Namespace: namespaceName, + Labels: componentLabel, + }, + Spec: v1beta1.DeploymentSpec{ + Replicas: &replicas, + Selector: nil, + Template: v1.PodTemplateSpec{ + ObjectMeta: v1.ObjectMeta{ + Name: svcName, + Labels: apiserverPodLabels, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "apiserver", + Image: image, + Command: []string{ + "/hyperkube", + "federation-apiserver", + "--bind-address=0.0.0.0", + "--etcd-servers=http://localhost:2379", + "--service-cluster-ip-range=10.0.0.0/16", + "--secure-port=443", + "--client-ca-file=/etc/federation/apiserver/ca.crt", + "--tls-cert-file=/etc/federation/apiserver/server.crt", + "--tls-private-key-file=/etc/federation/apiserver/server.key", + "--advertise-address=" + ip, + }, + Ports: []v1.ContainerPort{ + { + Name: "https", + ContainerPort: 443, + }, + { + Name: "local", + ContainerPort: 8080, + }, + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: credSecretName, + MountPath: "/etc/federation/apiserver", + ReadOnly: true, + }, + }, + }, + { + Name: "etcd", + Image: "quay.io/coreos/etcd:v2.3.3", + Command: []string{ + "/etcd", + "--data-dir", + "/var/etcd/data", + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "etcddata", + MountPath: "/var/etcd", + }, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: credSecretName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: credSecretName, + }, + }, + }, + { + Name: "etcddata", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: pvcName, + }, + }, + }, + }, + }, + }, + }, + } + + cmName := federationName + "-controller-manager" + cm := v1beta1.Deployment{ + TypeMeta: unversioned.TypeMeta{ + Kind: "Deployment", + APIVersion: testapi.Extensions.GroupVersion().String(), + }, + ObjectMeta: v1.ObjectMeta{ + Name: cmName, + Namespace: namespaceName, + Labels: componentLabel, + }, + Spec: v1beta1.DeploymentSpec{ + Replicas: &replicas, + Selector: nil, + Template: v1.PodTemplateSpec{ + ObjectMeta: v1.ObjectMeta{ + Name: cmName, + Labels: controllerManagerPodLabels, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "controller-manager", + Image: image, + Command: []string{ + "/hyperkube", + "federation-controller-manager", + "--master=https://federation-apiserver", + "--kubeconfig=/etc/federation/controller-manager/kubeconfig", + "--dns-provider=gce", + "--dns-provider-config=", + fmt.Sprintf("--federation-name=%s", federationName), + fmt.Sprintf("--zone-name=%s", dnsZoneName), + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: cmKubeconfigSecretName, + MountPath: "/etc/federation/controller-manager", + ReadOnly: true, + }, + }, + Env: []v1.EnvVar{ + { + Name: "POD_NAMESPACE", + ValueFrom: &v1.EnvVarSource{ + FieldRef: &v1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: cmKubeconfigSecretName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: cmKubeconfigSecretName, + }, + }, + }, + }, + }, + }, + }, + } + + f, tf, codec, _ := cmdtesting.NewAPIFactory() + extCodec := testapi.Extensions.Codec() + ns := dynamic.ContentConfig().NegotiatedSerializer + tf.ClientConfig = kubefedtesting.DefaultClientConfig() + tf.Client = &fake.RESTClient{ + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/api/v1/namespaces" && m == http.MethodPost: + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + var got v1.Namespace + _, _, err = codec.Decode(body, nil, &got) + if err != nil { + return nil, err + } + if !api.Semantic.DeepEqual(got, namespace) { + return nil, fmt.Errorf("Unexpected namespace object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, namespace)) + } + return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &namespace)}, nil + case p == svcUrlPrefix && m == http.MethodPost: + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + var got v1.Service + _, _, err = codec.Decode(body, nil, &got) + if err != nil { + return nil, err + } + if !api.Semantic.DeepEqual(got, svc) { + return nil, fmt.Errorf("Unexpected service object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, svc)) + } + return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &svc)}, nil + case strings.HasPrefix(p, svcUrlPrefix) && m == http.MethodGet: + got := strings.TrimPrefix(p, svcUrlPrefix+"/") + if got != svcName { + return nil, errors.NewNotFound(api.Resource("services"), got) + } + return &http.Response{StatusCode: http.StatusOK, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &svcWithLB)}, nil + case p == "/api/v1/namespaces/federation-system/secrets" && m == http.MethodPost: + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + var got, want v1.Secret + _, _, err = codec.Decode(body, nil, &got) + if err != nil { + return nil, err + } + // Obtained secret contains generated data which cannot + // be compared, so we just nullify the generated part + // and compare the rest of the secret. The generated + // parts are tested in other tests. + got.Data = nil + switch got.Name { + case credSecretName: + want = credSecret + case cmKubeconfigSecretName: + want = cmKubeconfigSecret + } + if !api.Semantic.DeepEqual(got, want) { + return nil, fmt.Errorf("Unexpected secret object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, want)) + } + return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &want)}, nil + case p == "/api/v1/namespaces/federation-system/persistentvolumeclaims" && m == http.MethodPost: + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + var got v1.PersistentVolumeClaim + _, _, err = codec.Decode(body, nil, &got) + if err != nil { + return nil, err + } + if !api.Semantic.DeepEqual(got, pvc) { + return nil, fmt.Errorf("Unexpected PVC object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, pvc)) + } + return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(codec, &pvc)}, nil + case p == "/apis/extensions/v1beta1/namespaces/federation-system/deployments" && m == http.MethodPost: + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + var got, want v1beta1.Deployment + _, _, err = codec.Decode(body, nil, &got) + if err != nil { + return nil, err + } + switch got.Name { + case svcName: + want = apiserver + case cmName: + want = cm + } + if !api.Semantic.DeepEqual(got, want) { + return nil, fmt.Errorf("Unexpected deployment object\n\tDiff: %s", diff.ObjectGoPrintDiff(got, want)) + } + return &http.Response{StatusCode: http.StatusCreated, Header: kubefedtesting.DefaultHeader(), Body: kubefedtesting.ObjBody(extCodec, &want)}, nil + default: + return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req) + } + }), + } + return f, nil +} + +func testKubeconfigUpdate(t *testing.T, federationName, lbIP, kubeconfigGlobal, kubeconfigExplicit string) { + filename := kubeconfigGlobal + if kubeconfigExplicit != "" { + filename = kubeconfigExplicit + } + config, err := clientcmd.LoadFromFile(filename) + if err != nil { + t.Errorf("Failed to open kubeconfig file: %v", err) + return + } + + cluster, ok := config.Clusters[federationName] + if !ok { + t.Errorf("No cluster info for %q", federationName) + return + } + endpoint := lbIP + if !strings.HasSuffix(lbIP, "https://") { + endpoint = fmt.Sprintf("https://%s", lbIP) + } + if cluster.Server != endpoint { + t.Errorf("Want federation API server endpoint %q, got %q", endpoint, cluster.Server) + } + + authInfo, ok := config.AuthInfos[federationName] + if !ok { + t.Errorf("No credentials for %q", federationName) + return + } + if len(authInfo.ClientCertificateData) == 0 { + t.Errorf("Expected client certificate to be non-empty") + return + } + if len(authInfo.ClientKeyData) == 0 { + t.Errorf("Expected client key to be non-empty") + return + } + if authInfo.Username != AdminCN { + t.Errorf("Want username: %q, got: %q", AdminCN, authInfo.Username) + } + + context, ok := config.Contexts[federationName] + if !ok { + t.Errorf("No context for %q", federationName) + return + } + if context.Cluster != federationName { + t.Errorf("Want context cluster name: %q, got: %q", federationName, context.Cluster) + } + if context.AuthInfo != federationName { + t.Errorf("Want context auth info: %q, got: %q", federationName, context.AuthInfo) + } +} + +type clientServerTLSConfigs struct { + server *tls.Config + client *tls.Config +} + +type certParams struct { + cAddr string + ips []string + hostnames []string +} + func tlsHandshake(t *testing.T, sCfg, cCfg *tls.Config) error { // Tried to use net.Pipe() instead of TCP. But the connections returned by // net.Pipe() do a fully-synchronous reads and writes on both the ends.