add utility for binding flags and building api server clients

This commit is contained in:
deads2k
2014-11-17 13:29:45 -05:00
parent ff1e9f4c19
commit 2dbfb80349
16 changed files with 764 additions and 190 deletions

View File

@@ -24,9 +24,10 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/golang/glog"
"github.com/spf13/cobra"
)
@@ -34,23 +35,29 @@ import (
// Factory provides abstractions that allow the Kubectl command to be extended across multiple types
// of resources and different API sets.
type Factory struct {
Mapper meta.RESTMapper
Typer runtime.ObjectTyper
Client func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error)
Describer func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error)
Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error)
ClientBuilder clientcmd.Builder
Mapper meta.RESTMapper
Typer runtime.ObjectTyper
Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error)
Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error)
Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error)
}
// NewFactory creates a factory with the default Kubernetes resources defined
func NewFactory() *Factory {
func NewFactory(clientBuilder clientcmd.Builder) *Factory {
return &Factory{
Mapper: latest.RESTMapper,
Typer: api.Scheme,
ClientBuilder: clientBuilder,
Mapper: latest.RESTMapper,
Typer: api.Scheme,
Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) {
return getKubeClient(cmd), nil
return clientBuilder.Client()
},
Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) {
describer, ok := kubectl.DescriberFor(mapping.Kind, getKubeClient(cmd))
client, err := clientBuilder.Client()
if err != nil {
return nil, err
}
describer, ok := kubectl.DescriberFor(mapping.Kind, client)
if !ok {
return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind)
}
@@ -73,23 +80,17 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
Run: runHelp,
}
f.ClientBuilder.BindFlags(cmds.PersistentFlags())
// Globally persistent flags across all subcommands.
// TODO Change flag names to consts to allow safer lookup from subcommands.
// TODO Add a verbose flag that turns on glog logging. Probably need a way
// to do that automatically for every subcommand.
cmds.PersistentFlags().StringP("server", "s", "", "Kubernetes apiserver to connect to")
cmds.PersistentFlags().StringP("auth-path", "a", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user. Only used if using https.")
cmds.PersistentFlags().Bool("match-server-version", false, "Require server version to match client version")
cmds.PersistentFlags().String("api-version", latest.Version, "The version of the API to use against the server")
cmds.PersistentFlags().String("certificate-authority", "", "Path to a certificate file for the certificate authority")
cmds.PersistentFlags().String("client-certificate", "", "Path to a client certificate for TLS.")
cmds.PersistentFlags().String("client-key", "", "Path to a client key file for TLS.")
cmds.PersistentFlags().Bool("insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.")
cmds.PersistentFlags().String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.")
cmds.PersistentFlags().StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.")
cmds.AddCommand(NewCmdVersion(out))
cmds.AddCommand(NewCmdProxy(out))
cmds.AddCommand(f.NewCmdVersion(out))
cmds.AddCommand(f.NewCmdProxy(out))
cmds.AddCommand(f.NewCmdGet(out))
cmds.AddCommand(f.NewCmdDescribe(out))
@@ -99,7 +100,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
cmds.AddCommand(f.NewCmdDelete(out))
cmds.AddCommand(NewCmdNamespace(out))
cmds.AddCommand(NewCmdLog(out))
cmds.AddCommand(f.NewCmdLog(out))
if err := cmds.Execute(); err != nil {
os.Exit(1)
@@ -150,69 +151,3 @@ func getExplicitKubeNamespace(cmd *cobra.Command) (string, bool) {
// value and return its value and true.
return "", false
}
// GetKubeConfig returns a config used for the Kubernetes client with CLI
// options parsed.
func GetKubeConfig(cmd *cobra.Command) *client.Config {
config := &client.Config{}
var host string
if hostFlag := GetFlagString(cmd, "server"); len(hostFlag) > 0 {
host = hostFlag
glog.V(2).Infof("Using server from -s flag: %s", host)
} else if len(os.Getenv("KUBERNETES_MASTER")) > 0 {
host = os.Getenv("KUBERNETES_MASTER")
glog.V(2).Infof("Using server from env var KUBERNETES_MASTER: %s", host)
} else {
// TODO: eventually apiserver should start on 443 and be secure by default
host = "http://localhost:8080"
glog.V(2).Infof("No server found in flag or env var, using default: %s", host)
}
config.Host = host
if client.IsConfigTransportTLS(config) {
// Get the values from the file on disk (or from the user at the
// command line). Override them with the command line parameters, if
// provided.
authPath := GetFlagString(cmd, "auth-path")
authInfo, err := kubectl.LoadClientAuthInfoOrPrompt(authPath, os.Stdin)
// TODO: handle the case where the file could not be written but
// we still got a user/pass from prompting.
if err != nil {
glog.Fatalf("Error loading auth: %v", err)
}
config.Username = authInfo.User
config.Password = authInfo.Password
// First priority is flag, then file.
config.CAFile = FirstNonEmptyString(GetFlagString(cmd, "certificate-authority"), authInfo.CAFile)
config.CertFile = FirstNonEmptyString(GetFlagString(cmd, "client-certificate"), authInfo.CertFile)
config.KeyFile = FirstNonEmptyString(GetFlagString(cmd, "client-key"), authInfo.KeyFile)
config.BearerToken = authInfo.BearerToken
// For config.Insecure, the command line ALWAYS overrides the authInfo
// file, regardless of its setting.
if insecureFlag := GetFlagBoolPtr(cmd, "insecure-skip-tls-verify"); insecureFlag != nil {
config.Insecure = *insecureFlag
} else if authInfo.Insecure != nil {
config.Insecure = *authInfo.Insecure
}
}
// The API version (e.g. v1beta1), not the binary version.
config.Version = GetFlagString(cmd, "api-version")
return config
}
func getKubeClient(cmd *cobra.Command) *client.Client {
config := GetKubeConfig(cmd)
// The binary version.
matchVersion := GetFlagBool(cmd, "match-server-version")
c, err := kubectl.GetKubeClient(config, matchVersion)
if err != nil {
glog.Fatalf("Error creating kubernetes client: %v", err)
}
return c
}

View File

@@ -21,7 +21,6 @@ import (
"io"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/config"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
@@ -80,8 +79,10 @@ Examples:
$ cat config.json | kubectl apply -f -
<creates all resources listed in config.json>`,
Run: func(cmd *cobra.Command, args []string) {
clientFunc := func(*meta.RESTMapping) (*client.RESTClient, error) {
return getKubeClient(cmd).RESTClient, nil
clientFunc := func(mapper *meta.RESTMapping) (config.RESTClientPoster, error) {
client, err := f.Client(cmd, mapper)
checkErr(err)
return client, nil
}
filename := GetFlagString(cmd, "filename")

View File

@@ -21,7 +21,7 @@ import (
"io"
)
func NewCmdLog(out io.Writer) *cobra.Command {
func (f *Factory) NewCmdLog(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "log <pod> <container>",
Short: "Print the logs for a container in a pod",
@@ -32,7 +32,8 @@ func NewCmdLog(out io.Writer) *cobra.Command {
namespace := getKubeNamespace(cmd)
client := getKubeClient(cmd)
client, err := f.ClientBuilder.Client()
checkErr(err)
pod, err := client.Pods(namespace).Get(args[0])
checkErr(err)

View File

@@ -24,7 +24,7 @@ import (
"github.com/spf13/cobra"
)
func NewCmdProxy(out io.Writer) *cobra.Command {
func (f *Factory) NewCmdProxy(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "proxy",
Short: "Run a proxy to the Kubernetes API server",
@@ -32,7 +32,11 @@ func NewCmdProxy(out io.Writer) *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
port := GetFlagInt(cmd, "port")
glog.Infof("Starting to serve on localhost:%d", port)
server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), GetKubeConfig(cmd), port)
clientConfig, err := f.ClientBuilder.Config()
checkErr(err)
server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), clientConfig, port)
checkErr(err)
glog.Fatal(server.Serve())
},

View File

@@ -23,7 +23,7 @@ import (
"github.com/spf13/cobra"
)
func NewCmdVersion(out io.Writer) *cobra.Command {
func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Print version of client and server",
@@ -31,7 +31,10 @@ func NewCmdVersion(out io.Writer) *cobra.Command {
if GetFlagBool(cmd, "client") {
kubectl.GetClientVersion(out)
} else {
kubectl.GetVersion(out, getKubeClient(cmd))
client, err := f.ClientBuilder.Client()
checkErr(err)
kubectl.GetVersion(out, client)
}
},
}

View File

@@ -20,7 +20,6 @@ package kubectl
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
@@ -28,7 +27,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
@@ -90,34 +88,6 @@ func SaveNamespaceInfo(path string, ns *NamespaceInfo) error {
return err
}
// LoadClientAuthInfoOrPrompt parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist.
func LoadClientAuthInfoOrPrompt(path string, r io.Reader) (*clientauth.Info, error) {
var auth clientauth.Info
// Prompt for user/pass and write a file if none exists.
if _, err := os.Stat(path); os.IsNotExist(err) {
auth.User = promptForString("Username", r)
auth.Password = promptForString("Password", r)
data, err := json.Marshal(auth)
if err != nil {
return &auth, err
}
err = ioutil.WriteFile(path, data, 0600)
return &auth, err
}
authPtr, err := clientauth.LoadFromFile(path)
if err != nil {
return nil, err
}
return authPtr, nil
}
func promptForString(field string, r io.Reader) string {
fmt.Printf("Please enter %s: ", field)
var result string
fmt.Fscan(r, &result)
return result
}
// TODO Move to labels package.
func formatLabels(labelMap map[string]string) string {
l := labels.Set(labelMap).String()

View File

@@ -17,15 +17,12 @@ limitations under the License.
package kubectl
import (
"bytes"
"io"
"io/ioutil"
"os"
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth"
)
func validateAction(expectedAction, actualAction client.FakeAction, t *testing.T) {
@@ -85,56 +82,3 @@ func TestLoadNamespaceInfo(t *testing.T) {
}
}
}
func TestLoadClientAuthInfoOrPrompt(t *testing.T) {
loadAuthInfoTests := []struct {
authData string
authInfo *clientauth.Info
r io.Reader
}{
{
`{"user": "user", "password": "pass"}`,
&clientauth.Info{User: "user", Password: "pass"},
nil,
},
{
"", nil, nil,
},
{
"missing",
&clientauth.Info{User: "user", Password: "pass"},
bytes.NewBufferString("user\npass"),
},
}
for _, loadAuthInfoTest := range loadAuthInfoTests {
tt := loadAuthInfoTest
aifile, err := ioutil.TempFile("", "testAuthInfo")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if tt.authData != "missing" {
defer os.Remove(aifile.Name())
defer aifile.Close()
_, err = aifile.WriteString(tt.authData)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
} else {
aifile.Close()
os.Remove(aifile.Name())
}
authInfo, err := LoadClientAuthInfoOrPrompt(aifile.Name(), tt.r)
if len(tt.authData) == 0 && tt.authData != "missing" {
if err == nil {
t.Error("LoadClientAuthInfoOrPrompt didn't fail on empty file")
}
continue
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !reflect.DeepEqual(authInfo, tt.authInfo) {
t.Errorf("Expected %#v, got %#v", tt.authInfo, authInfo)
}
}
}