diff --git a/federation/pkg/kubefed/BUILD b/federation/pkg/kubefed/BUILD new file mode 100644 index 00000000000..dc5914b9005 --- /dev/null +++ b/federation/pkg/kubefed/BUILD @@ -0,0 +1,52 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_binary", + "go_library", + "go_test", + "cgo_library", +) + +go_library( + name = "go_default_library", + srcs = ["join.go"], + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/client/unversioned/clientcmd:go_default_library", + "//pkg/client/unversioned/clientcmd/api:go_default_library", + "//pkg/kubectl:go_default_library", + "//pkg/kubectl/cmd:go_default_library", + "//pkg/kubectl/cmd/templates:go_default_library", + "//pkg/kubectl/cmd/util:go_default_library", + "//pkg/runtime:go_default_library", + "//vendor:github.com/golang/glog", + "//vendor:github.com/spf13/cobra", + ], +) + +go_test( + name = "go_default_test", + srcs = ["join_test.go"], + library = "go_default_library", + tags = ["automanaged"], + deps = [ + "//federation/apis/federation/v1beta1:go_default_library", + "//pkg/api:go_default_library", + "//pkg/api/testapi:go_default_library", + "//pkg/api/unversioned:go_default_library", + "//pkg/api/v1:go_default_library", + "//pkg/apimachinery/registered:go_default_library", + "//pkg/client/restclient: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/client/unversioned/clientcmd/api:go_default_library", + "//pkg/kubectl/cmd/testing:go_default_library", + "//pkg/kubectl/cmd/util:go_default_library", + "//pkg/runtime:go_default_library", + ], +) diff --git a/federation/pkg/kubefed/join.go b/federation/pkg/kubefed/join.go new file mode 100644 index 00000000000..91bfd2c7072 --- /dev/null +++ b/federation/pkg/kubefed/join.go @@ -0,0 +1,300 @@ +/* +Copyright 2016 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 kubefed + +import ( + "fmt" + "io" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" + clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" + "k8s.io/kubernetes/pkg/kubectl" + kubectlcmd "k8s.io/kubernetes/pkg/kubectl/cmd" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/runtime" + + "github.com/golang/glog" + "github.com/spf13/cobra" +) + +const ( + // KubeconfigSecretDataKey is the key name used in the secret to + // stores a cluster's credentials. + KubeconfigSecretDataKey = "kubeconfig" + + // defaultClusterCIDR is the default CIDR range accepted by the + // joining API server. See `apis/federation.ClusterSpec` for + // details. + // TODO(madhusudancs): Make this value customizable. + defaultClientCIDR = "0.0.0.0/0" +) + +var ( + join_long = templates.LongDesc(` + Join a cluster to a federation. + + Current context is assumed to be a federation API + server. Please use the --context flag otherwise.`) + join_example = templates.Examples(` + # Join a cluster to a federation by specifying the + # cluster context name and the context name of the + # federation control plane's host cluster. + kubectl join foo --host=bar`) +) + +// JoinFederationConfig provides a filesystem based kubeconfig (via +// `PathOptions()`) and a mechanism to talk to the federation host +// cluster. +type JoinFederationConfig interface { + // PathOptions provides filesystem based kubeconfig access. + PathOptions() *clientcmd.PathOptions + // HostFactory provides a mechanism to communicate with the + // cluster where federation control plane is hosted. + HostFactory(host, kubeconfigPath string) cmdutil.Factory +} + +// joinFederationConfig implements JoinFederationConfig interface. +type joinFederationConfig struct { + pathOptions *clientcmd.PathOptions +} + +// Assert that `joinFederationConfig` implements the +// `JoinFederationConfig` interface. +var _ JoinFederationConfig = &joinFederationConfig{} + +func NewJoinFederationConfig(pathOptions *clientcmd.PathOptions) JoinFederationConfig { + return &joinFederationConfig{ + pathOptions: pathOptions, + } +} + +func (j *joinFederationConfig) PathOptions() *clientcmd.PathOptions { + return j.pathOptions +} + +func (j *joinFederationConfig) HostFactory(host, kubeconfigPath string) cmdutil.Factory { + loadingRules := *j.pathOptions.LoadingRules + loadingRules.Precedence = j.pathOptions.GetLoadingPrecedence() + loadingRules.ExplicitPath = kubeconfigPath + overrides := &clientcmd.ConfigOverrides{ + CurrentContext: host, + } + + hostClientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, overrides) + + return cmdutil.NewFactory(hostClientConfig) +} + +// NewCmdJoin defines the `join` command that joins a cluster to a +// federation. +func NewCmdJoin(f cmdutil.Factory, cmdOut io.Writer, config JoinFederationConfig) *cobra.Command { + cmd := &cobra.Command{ + Use: "join CLUSTER_CONTEXT --host=HOST_CONTEXT", + Short: "Join a cluster to a federation", + Long: join_long, + Example: join_example, + Run: func(cmd *cobra.Command, args []string) { + err := joinFederation(f, cmdOut, config, cmd, args) + cmdutil.CheckErr(err) + }, + } + + cmdutil.AddApplyAnnotationFlags(cmd) + cmdutil.AddValidateFlags(cmd) + cmdutil.AddPrinterFlags(cmd) + cmdutil.AddGeneratorFlags(cmd, cmdutil.ClusterV1Beta1GeneratorName) + addJoinFlags(cmd) + return cmd +} + +// joinFederation is the implementation of the `join federation` command. +func joinFederation(f cmdutil.Factory, cmdOut io.Writer, config JoinFederationConfig, cmd *cobra.Command, args []string) error { + name, err := kubectlcmd.NameFromCommandArgs(cmd, args) + if err != nil { + return err + } + host := cmdutil.GetFlagString(cmd, "host") + hostSystemNamespace := cmdutil.GetFlagString(cmd, "host-system-namespace") + kubeconfig := cmdutil.GetFlagString(cmd, "kubeconfig") + dryRun := cmdutil.GetDryRunFlag(cmd) + + glog.V(2).Infof("Args and flags: name %s, host: %s, host-system-namespace: %s, kubeconfig: %s, dry-run: %s", name, host, hostSystemNamespace, kubeconfig, dryRun) + + po := config.PathOptions() + po.LoadingRules.ExplicitPath = kubeconfig + clientConfig, err := po.GetStartingConfig() + if err != nil { + return err + } + generator, err := clusterGenerator(clientConfig, name) + if err != nil { + glog.V(2).Infof("Failed creating cluster generator: %v", err) + return err + } + glog.V(2).Infof("Created cluster generator: %#v", generator) + + // We are not using the `kubectl create secret` machinery through + // `RunCreateSubcommand` as we do to the cluster resource below + // because we have a bunch of requirements that the machinery does + // not satisfy. + // 1. We want to create the secret in a specific namespace, which + // is neither the "default" namespace nor the one specified + // via the `--namespace` flag. + // 2. `SecretGeneratorV1` requires LiteralSources in a string-ified + // form that it parses to generate the secret data key-value + // pairs. We, however, have the key-value pairs ready without a + // need for parsing. + // 3. The result printing mechanism needs to be mostly quiet. We + // don't have to print the created secret in the default case. + // Having said that, secret generation machinery could be altered to + // suit our needs, but it is far less invasive and readable this way. + hostFactory := config.HostFactory(host, kubeconfig) + _, err = createSecret(hostFactory, clientConfig, hostSystemNamespace, name, dryRun) + if err != nil { + glog.V(2).Infof("Failed creating the cluster credentials secret: %v", err) + return err + } + glog.V(2).Infof("Cluster credentials secret created") + + return kubectlcmd.RunCreateSubcommand(f, cmd, cmdOut, &kubectlcmd.CreateSubcommandOptions{ + Name: name, + StructuredGenerator: generator, + DryRun: dryRun, + OutputFormat: cmdutil.GetFlagString(cmd, "output"), + }) +} + +func addJoinFlags(cmd *cobra.Command) { + cmd.Flags().String("kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + cmd.Flags().String("host", "", "Host cluster context") + cmd.Flags().String("host-system-namespace", "federation-system", "Namespace in the host cluster where the federation system components are installed") +} + +// minifyConfig is a wrapper around `clientcmdapi.MinifyConfig()` that +// sets the current context to the given context before calling +// `clientcmdapi.MinifyConfig()`. +func minifyConfig(clientConfig *clientcmdapi.Config, context string) (*clientcmdapi.Config, error) { + // MinifyConfig inline-modifies the passed clientConfig. So we make a + // copy of it before passing the config to it. A shallow copy is + // sufficient because the underlying fields will be reconstructed by + // MinifyConfig anyway. + newClientConfig := *clientConfig + newClientConfig.CurrentContext = context + err := clientcmdapi.MinifyConfig(&newClientConfig) + if err != nil { + return nil, err + } + return &newClientConfig, nil +} + +// createSecret extracts the kubeconfig for a given cluster and populates +// a secret with that kubeconfig. +func createSecret(hostFactory cmdutil.Factory, clientConfig *clientcmdapi.Config, namespace, name string, dryRun bool) (runtime.Object, error) { + // Minify the kubeconfig to ensure that there is only information + // relevant to the cluster we are registering. + newClientConfig, err := minifyConfig(clientConfig, name) + if err != nil { + glog.V(2).Infof("Failed to minify the kubeconfig for the given context %q: %v", name, err) + return nil, err + } + + // Flatten the kubeconfig to ensure that all the referenced file + // contents are inlined. + err = clientcmdapi.FlattenConfig(newClientConfig) + if err != nil { + glog.V(2).Infof("Failed to flatten the kubeconfig for the given context %q: %v", name, err) + return nil, err + } + + configBytes, err := clientcmd.Write(*newClientConfig) + if err != nil { + glog.V(2).Infof("Failed to serialize the kubeconfig for the given context %q: %v", name, err) + return nil, err + } + + // Build the secret object with the minified and flattened + // kubeconfig content. + secret := &api.Secret{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{ + KubeconfigSecretDataKey: configBytes, + }, + } + + if !dryRun { + // Boilerplate to create the secret in the host cluster. + clientset, err := hostFactory.ClientSet() + if err != nil { + glog.V(2).Infof("Failed to retrieve the cluster clientset: %v", err) + return nil, err + } + return clientset.Core().Secrets(namespace).Create(secret) + } + return secret, nil +} + +// clusterGenerator extracts the cluster information from the supplied +// kubeconfig and builds a StructuredGenerator for the +// `federation/cluster` API resource. +func clusterGenerator(clientConfig *clientcmdapi.Config, name string) (kubectl.StructuredGenerator, error) { + // Get the context from the config. + ctx, found := clientConfig.Contexts[name] + if !found { + return nil, fmt.Errorf("cluster context %q not found", name) + } + + // Get the cluster object corresponding to the supplied context. + cluster, found := clientConfig.Clusters[ctx.Cluster] + if !found { + return nil, fmt.Errorf("cluster endpoint not found for %q", name) + } + + // Extract the scheme portion of the cluster APIServer endpoint and + // default it to `https` if it isn't specified. + scheme := extractScheme(cluster.Server) + serverAddress := cluster.Server + if scheme == "" { + // Use "https" as the default scheme. + scheme := "https" + serverAddress = strings.Join([]string{scheme, serverAddress}, "://") + } + + generator := &kubectl.ClusterGeneratorV1Beta1{ + Name: name, + ClientCIDR: defaultClientCIDR, + ServerAddress: serverAddress, + SecretName: name, + } + return generator, nil +} + +// extractScheme parses the given URL to extract the scheme portion +// out of it. +func extractScheme(url string) string { + scheme := "" + segs := strings.SplitN(url, "://", 2) + if len(segs) == 2 { + scheme = segs[0] + } + return scheme +} diff --git a/federation/pkg/kubefed/join_test.go b/federation/pkg/kubefed/join_test.go new file mode 100644 index 00000000000..cfeb7ef23da --- /dev/null +++ b/federation/pkg/kubefed/join_test.go @@ -0,0 +1,392 @@ +/* +Copyright 2016 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 kubefed + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "testing" + + federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/apimachinery/registered" + "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/client/restclient/fake" + "k8s.io/kubernetes/pkg/client/typed/dynamic" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" + clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" + cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/runtime" +) + +func TestJoinFederation(t *testing.T) { + cmdErrMsg := "" + cmdutil.BehaviorOnFatal(func(str string, code int) { + cmdErrMsg = str + }) + + fakeKubeFiles, err := fakeKubeconfigFiles() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer rmFakeKubeconfigFiles(fakeKubeFiles) + + testCases := []struct { + cluster string + server string + token string + kubeconfigGlobal string + kubeconfigExplicit string + expectedServer string + expectedErr string + }{ + { + cluster: "syndicate", + server: "https://10.20.30.40", + token: "badge", + kubeconfigGlobal: fakeKubeFiles[0], + kubeconfigExplicit: "", + expectedServer: "https://10.20.30.40", + expectedErr: "", + }, + { + cluster: "ally", + server: "ally256.example.com:80", + token: "souvenir", + kubeconfigGlobal: fakeKubeFiles[0], + kubeconfigExplicit: fakeKubeFiles[1], + expectedServer: "https://ally256.example.com:80", + expectedErr: "", + }, + { + cluster: "confederate", + server: "10.8.8.8", + token: "totem", + kubeconfigGlobal: fakeKubeFiles[1], + kubeconfigExplicit: fakeKubeFiles[2], + expectedServer: "https://10.8.8.8", + expectedErr: "", + }, + { + cluster: "affiliate", + server: "https://10.20.30.40", + token: "badge", + kubeconfigGlobal: fakeKubeFiles[0], + kubeconfigExplicit: "", + expectedServer: "https://10.20.30.40", + expectedErr: fmt.Sprintf("error: cluster context %q not found", "affiliate"), + }, + } + + for i, tc := range testCases { + cmdErrMsg = "" + f := testJoinFederationFactory(tc.cluster, tc.expectedServer) + buf := bytes.NewBuffer([]byte{}) + + hostFactory, err := fakeJoinHostFactory(tc.cluster, tc.server, tc.token) + if err != nil { + t.Fatalf("[%d] unexpected error: %v", i, err) + } + + joinConfig, err := newFakeJoinFederationConfig(hostFactory, tc.kubeconfigGlobal) + if err != nil { + t.Fatalf("[%d] unexpected error: %v", i, err) + } + + cmd := NewCmdJoin(f, buf, joinConfig) + + cmd.Flags().Set("kubeconfig", tc.kubeconfigExplicit) + cmd.Flags().Set("host", "substrate") + cmd.Run(cmd, []string{tc.cluster}) + + if tc.expectedErr == "" { + // uses the name from the cluster, not the response + // Actual data passed are tested in the fake secret and cluster + // REST clients. + if msg := buf.String(); msg != fmt.Sprintf("cluster %q created\n", tc.cluster) { + t.Errorf("[%d] unexpected output: %s", i, msg) + 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()) + } + } + } +} + +func testJoinFederationFactory(name, server string) cmdutil.Factory { + want := fakeCluster(name, server) + f, tf, _, _ := cmdtesting.NewAPIFactory() + codec := testapi.Federation.Codec() + ns := dynamic.ContentConfig().NegotiatedSerializer + 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 == "/clusters" && m == http.MethodPost: + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + var got federationapi.Cluster + _, _, err = codec.Decode(body, nil, &got) + if err != nil { + return nil, err + } + if !api.Semantic.DeepEqual(got, want) { + return nil, fmt.Errorf("unexpected cluster object\n\tgot: %#v\n\twant: %#v", got, want) + } + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &want)}, nil + default: + return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req) + } + }), + } + tf.Namespace = "test" + return f +} + +type fakeJoinFederationConfig struct { + pathOptions *clientcmd.PathOptions + hostFactory cmdutil.Factory +} + +func newFakeJoinFederationConfig(f cmdutil.Factory, kubeconfigGlobal string) (JoinFederationConfig, error) { + pathOptions := clientcmd.NewDefaultPathOptions() + pathOptions.GlobalFile = kubeconfigGlobal + pathOptions.EnvVar = "" + + return &fakeJoinFederationConfig{ + pathOptions: pathOptions, + hostFactory: f, + }, nil +} + +func (r *fakeJoinFederationConfig) PathOptions() *clientcmd.PathOptions { + return r.pathOptions +} + +func (r *fakeJoinFederationConfig) HostFactory(host, kubeconfigPath string) cmdutil.Factory { + return r.hostFactory +} + +func fakeJoinHostFactory(name, server, token string) (cmdutil.Factory, error) { + kubeconfig := clientcmdapi.Config{ + Clusters: map[string]*clientcmdapi.Cluster{ + name: { + Server: server, + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + name: { + Token: token, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + name: { + Cluster: name, + AuthInfo: name, + }, + }, + CurrentContext: name, + } + configBytes, err := clientcmd.Write(kubeconfig) + if err != nil { + return nil, err + } + secretObject := v1.Secret{ + TypeMeta: unversioned.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: "federation-system", + }, + Data: map[string][]byte{ + "kubeconfig": configBytes, + }, + } + + f, tf, codec, _ := cmdtesting.NewAPIFactory() + ns := dynamic.ContentConfig().NegotiatedSerializer + tf.ClientConfig = 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/federation-system/secrets" && m == http.MethodPost: + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + var got v1.Secret + _, _, err = codec.Decode(body, nil, &got) + if err != nil { + return nil, err + } + if !api.Semantic.DeepEqual(got, secretObject) { + return nil, fmt.Errorf("Unexpected secret object\n\tgot: %#v\n\twant: %#v", got, secretObject) + } + return &http.Response{StatusCode: http.StatusCreated, Header: defaultHeader(), Body: objBody(codec, &secretObject)}, nil + default: + return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req) + } + }), + } + return f, nil +} + +func fakeCluster(name, server string) federationapi.Cluster { + return federationapi.Cluster{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + }, + Spec: federationapi.ClusterSpec{ + ServerAddressByClientCIDRs: []federationapi.ServerAddressByClientCIDR{ + { + ClientCIDR: defaultClientCIDR, + ServerAddress: server, + }, + }, + SecretRef: &v1.LocalObjectReference{ + Name: name, + }, + }, + } +} + +func fakeKubeconfigFiles() ([]string, error) { + kubeconfigs := []clientcmdapi.Config{ + { + Clusters: map[string]*clientcmdapi.Cluster{ + "syndicate": { + Server: "https://10.20.30.40", + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "syndicate": { + Token: "badge", + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "syndicate": { + Cluster: "syndicate", + AuthInfo: "syndicate", + }, + }, + CurrentContext: "syndicate", + }, + { + Clusters: map[string]*clientcmdapi.Cluster{ + "ally": { + Server: "ally256.example.com:80", + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "ally": { + Token: "souvenir", + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "ally": { + Cluster: "ally", + AuthInfo: "ally", + }, + }, + CurrentContext: "ally", + }, + { + Clusters: map[string]*clientcmdapi.Cluster{ + "ally": { + Server: "https://ally64.example.com", + }, + "confederate": { + Server: "10.8.8.8", + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "ally": { + Token: "souvenir", + }, + "confederate": { + Token: "totem", + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "ally": { + Cluster: "ally", + AuthInfo: "ally", + }, + "confederate": { + Cluster: "confederate", + AuthInfo: "confederate", + }, + }, + CurrentContext: "confederate", + }, + } + kubefiles := []string{} + for _, cfg := range kubeconfigs { + fakeKubeFile, _ := ioutil.TempFile("", "") + err := clientcmd.WriteToFile(cfg, fakeKubeFile.Name()) + if err != nil { + return nil, err + } + + kubefiles = append(kubefiles, fakeKubeFile.Name()) + } + return kubefiles, nil +} + +func rmFakeKubeconfigFiles(kubefiles []string) { + for _, file := range kubefiles { + os.Remove(file) + } +} + +func defaultHeader() http.Header { + header := http.Header{} + header.Set("Content-Type", runtime.ContentTypeJSON) + return header +} + +func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { + return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) +} + +func defaultClientConfig() *restclient.Config { + return &restclient.Config{ + APIPath: "/api", + ContentConfig: restclient.ContentConfig{ + NegotiatedSerializer: api.Codecs, + ContentType: runtime.ContentTypeJSON, + GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion, + }, + } +} diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 5a2c7edb66d..693d6f28d0f 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -242,6 +242,7 @@ host-ipc-sources host-network-sources host-pid-sources host-port-endpoints +host-system-namespace hostname-override http-check-frequency http-port diff --git a/test/test_owners.csv b/test/test_owners.csv index fe2c129d709..91a8f22d033 100644 --- a/test/test_owners.csv +++ b/test/test_owners.csv @@ -480,6 +480,7 @@ k8s.io/kubernetes/federation/pkg/federation-controller/util,bgrant0607,1 k8s.io/kubernetes/federation/pkg/federation-controller/util/eventsink,luxas,1 k8s.io/kubernetes/federation/pkg/federation-controller/util/planner,Q-Lee,1 k8s.io/kubernetes/federation/pkg/federation-controller/util/podanalyzer,caesarxuchao,1 +k8s.io/kubernetes/federation/pkg/kubefed,madhusudancs,0 k8s.io/kubernetes/federation/registry/cluster,nikhiljindal,0 k8s.io/kubernetes/federation/registry/cluster/etcd,nikhiljindal,0 k8s.io/kubernetes/hack/cmd/teststale,thockin,1