diff --git a/cmd/kubectl/kubectl.go b/cmd/kubectl/kubectl.go new file mode 100644 index 00000000000..941ec363df5 --- /dev/null +++ b/cmd/kubectl/kubectl.go @@ -0,0 +1,27 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 main + +import ( + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" +) + +func main() { + cmd.RunKubectl(os.Stdout) +} diff --git a/hack/config-go.sh b/hack/config-go.sh index 2fdae81576d..d0b3479a773 100644 --- a/hack/config-go.sh +++ b/hack/config-go.sh @@ -160,6 +160,7 @@ kube::default_build_targets() { echo "cmd/e2e" echo "cmd/kubelet" echo "cmd/kubecfg" + echo "cmd/kubectl" echo "plugin/cmd/scheduler" } diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 8e97bad737e..ca5e1f50900 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -51,9 +51,9 @@ API_HOST=${API_HOST:-127.0.0.1} KUBELET_PORT=${KUBELET_PORT:-10250} GO_OUT=${KUBE_TARGET}/bin -# Check kubecfg -out=$("${GO_OUT}/kubecfg" -version) -echo kubecfg: $out +# Check kubectl +out=$("${GO_OUT}/kubectl") +echo kubectl: $out # Start kubelet ${GO_OUT}/kubelet \ @@ -76,19 +76,20 @@ APISERVER_PID=$! wait_for_url "http://127.0.0.1:${API_PORT}/healthz" "apiserver: " -KUBE_CMD="${GO_OUT}/kubecfg -h http://127.0.0.1:${API_PORT} -expect_version_match" +KUBE_CMD="${GO_OUT}/kubectl" +KUBE_FLAGS="-s http://127.0.0.1:${API_PORT} --match-server-version" -${KUBE_CMD} list pods -echo "kubecfg(pods): ok" +${KUBE_CMD} get pods ${KUBE_FLAGS} +echo "kubectl(pods): ok" -${KUBE_CMD} list services -${KUBE_CMD} -c examples/guestbook/frontend-service.json create services -${KUBE_CMD} delete services/frontend -echo "kubecfg(services): ok" +${KUBE_CMD} get services ${KUBE_FLAGS} +${KUBE_CMD} create -f examples/guestbook/frontend-service.json ${KUBE_FLAGS} +${KUBE_CMD} delete service frontend ${KUBE_FLAGS} +echo "kubectl(services): ok" -${KUBE_CMD} list minions -${KUBE_CMD} get minions/127.0.0.1 -echo "kubecfg(minions): ok" +${KUBE_CMD} get minions ${KUBE_FLAGS} +${KUBE_CMD} get minions 127.0.0.1 ${KUBE_FLAGS} +echo "kubectl(minions): ok" # Start controller manager #${GO_OUT}/controller-manager \ diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go new file mode 100644 index 00000000000..14760b199a0 --- /dev/null +++ b/pkg/kubectl/cmd/cmd.go @@ -0,0 +1,255 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strconv" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/golang/glog" + "github.com/spf13/cobra" +) + +func RunKubectl(out io.Writer) { + // Parent command to which all subcommands are added. + cmds := &cobra.Command{ + Use: "kubectl", + Short: "kubectl controls the Kubernetes cluster manager", + Long: `kubectl controls the Kubernetes cluster manager. + +Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, + Run: runHelp, + } + + // 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 (used for viewing resources only)") + 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.AddCommand(NewCmdVersion(out)) + cmds.AddCommand(NewCmdProxy(out)) + cmds.AddCommand(NewCmdGet(out)) + cmds.AddCommand(NewCmdDescribe(out)) + cmds.AddCommand(NewCmdCreate(out)) + cmds.AddCommand(NewCmdUpdate(out)) + cmds.AddCommand(NewCmdDelete(out)) + + if err := cmds.Execute(); err != nil { + os.Exit(1) + } +} + +func checkErr(err error) { + if err != nil { + glog.Fatalf("%v", err) + } +} + +func usageError(cmd *cobra.Command, format string, args ...interface{}) { + glog.Errorf(format, args...) + glog.Errorf("See '%s -h' for help.", cmd.CommandPath()) + os.Exit(1) +} + +func runHelp(cmd *cobra.Command, args []string) { + cmd.Help() +} + +func getFlagString(cmd *cobra.Command, flag string) string { + f := cmd.Flags().Lookup(flag) + if f == nil { + glog.Fatalf("Flag accessed but not defined for command %s: %s", cmd.Name(), flag) + } + return f.Value.String() +} + +func getFlagBool(cmd *cobra.Command, flag string) bool { + f := cmd.Flags().Lookup(flag) + if f == nil { + glog.Fatalf("Flag accessed but not defined for command %s: %s", cmd.Name(), flag) + } + // Caseless compare. + if strings.ToLower(f.Value.String()) == "true" { + return true + } + return false +} + +// Returns nil if the flag wasn't set. +func getFlagBoolPtr(cmd *cobra.Command, flag string) *bool { + f := cmd.Flags().Lookup(flag) + if f == nil { + glog.Fatalf("Flag accessed but not defined for command %s: %s", cmd.Name(), flag) + } + // Check if flag was not set at all. + if !f.Changed && f.DefValue == f.Value.String() { + return nil + } + var ret bool + // Caseless compare. + if strings.ToLower(f.Value.String()) == "true" { + ret = true + } else { + ret = false + } + return &ret +} + +// Assumes the flag has a default value. +func getFlagInt(cmd *cobra.Command, flag string) int { + f := cmd.Flags().Lookup(flag) + if f == nil { + glog.Fatalf("Flag accessed but not defined for command %s: %s", cmd.Name(), flag) + } + v, err := strconv.Atoi(f.Value.String()) + // This is likely not a sufficiently friendly error message, but cobra + // should prevent non-integer values from reaching here. + checkErr(err) + return v +} + +func getKubeClient(cmd *cobra.Command) *client.Client { + 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.IsConfigTransportSecure(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.LoadAuthInfo(authPath, os.Stdin) + 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) + // 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") + + // 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 +} + +// Returns the first non-empty string out of the ones provided. If all +// strings are empty, returns an empty string. +func firstNonEmptyString(args ...string) string { + for _, s := range args { + if len(s) > 0 { + return s + } + } + return "" +} + +// readConfigData reads the bytes from the specified filesytem or network +// location or from stdin if location == "-". +func readConfigData(location string) ([]byte, error) { + if len(location) == 0 { + return nil, fmt.Errorf("Location given but empty") + } + + if location == "-" { + // Read from stdin. + data, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return nil, err + } + + if len(data) == 0 { + return nil, fmt.Errorf(`Read from stdin specified ("-") but no data found`) + } + + return data, nil + } + + // Use the location as a file path or URL. + return readConfigDataFromLocation(location) +} + +func readConfigDataFromLocation(location string) ([]byte, error) { + // we look for http:// or https:// to determine if valid URL, otherwise do normal file IO + if strings.Index(location, "http://") == 0 || strings.Index(location, "https://") == 0 { + resp, err := http.Get(location) + if err != nil { + return nil, fmt.Errorf("Unable to access URL %s: %v\n", location, err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, fmt.Errorf("Unable to read URL, server reported %d %s", resp.StatusCode, resp.Status) + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Unable to read URL %s: %v\n", location, err) + } + return data, nil + } else { + data, err := ioutil.ReadFile(location) + if err != nil { + return nil, fmt.Errorf("Unable to read %s: %v\n", location, err) + } + return data, nil + } +} diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go new file mode 100644 index 00000000000..95bf92522cf --- /dev/null +++ b/pkg/kubectl/cmd/create.go @@ -0,0 +1,54 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/spf13/cobra" +) + +func NewCmdCreate(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "create -f filename", + Short: "Create a resource by filename or stdin", + Long: `Create a resource by filename or stdin. + +JSON and YAML formats are accepted. + +Examples: + $ kubectl create -f pod.json + + + $ cat pod.json | kubectl create -f - + `, + Run: func(cmd *cobra.Command, args []string) { + filename := getFlagString(cmd, "filename") + if len(filename) == 0 { + usageError(cmd, "Must pass a filename to update") + } + data, err := readConfigData(filename) + checkErr(err) + + err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, kubectl.ModifyCreate, data) + checkErr(err) + }, + } + cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to create the resource") + return cmd +} diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go new file mode 100644 index 00000000000..a6cd1b70511 --- /dev/null +++ b/pkg/kubectl/cmd/delete.go @@ -0,0 +1,80 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/spf13/cobra" +) + +func NewCmdDelete(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ([-f filename] | ( ))", + Short: "Delete a resource by filename, stdin or resource and id", + Long: `Delete a resource by filename, stdin or resource and id. + +JSON and YAML formats are accepted. + +If both a filename and command line arguments are passed, the command line +arguments are used and the filename is ignored. + +Note that the delete command does NOT do resource version checks, so if someone +submits an update to a resource right when you submit a delete, their update +will be lost along with the rest of the resource. + +Examples: + $ kubectl delete -f pod.json + + + $ cat pod.json | kubectl delete -f - + + + $ kubectl delete pod 1234-56-7890-234234-456456 + `, + Run: func(cmd *cobra.Command, args []string) { + // If command line args are passed in, use those preferentially. + if len(args) > 0 && len(args) != 2 { + usageError(cmd, "If passing in command line parameters, must be resource and id") + } + + var data []byte + var err error + + if len(args) == 2 { + data, err = kubectl.CreateResource(args[0], args[1]) + } else { + filename := getFlagString(cmd, "filename") + if len(filename) > 0 { + data, err = readConfigData(getFlagString(cmd, "filename")) + } + } + checkErr(err) + + if len(data) == 0 { + usageError(cmd, "Must specify filename or command line params") + } + + // TODO Add ability to require a resource-version check for delete. + err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, kubectl.ModifyDelete, data) + checkErr(err) + }, + } + cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to delete the resource") + return cmd +} diff --git a/pkg/kubectl/cmd/describe.go b/pkg/kubectl/cmd/describe.go new file mode 100644 index 00000000000..863a3a3e26e --- /dev/null +++ b/pkg/kubectl/cmd/describe.go @@ -0,0 +1,45 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/spf13/cobra" +) + +func NewCmdDescribe(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe ", + Short: "Show details of a specific resource", + Long: `Show details of a specific resource. + +This command joins many API calls together to form a detailed description of a +given resource.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 2 { + usageError(cmd, "Need to supply a resource and an ID") + } + resource := args[0] + id := args[1] + err := kubectl.Describe(out, getKubeClient(cmd), resource, id) + checkErr(err) + }, + } + return cmd +} diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go new file mode 100644 index 00000000000..3a7986304be --- /dev/null +++ b/pkg/kubectl/cmd/get.go @@ -0,0 +1,70 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/spf13/cobra" +) + +func NewCmdGet(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "get [(-o|--output=)table|json|yaml|template] [-t |--template=] []", + Short: "Display one or many resources", + Long: `Display one or many resources. + +Possible resources include pods (po), replication controllers (rc), services +(se) or minions (mi). + +If you specify a Go template, you can use any field defined in pkg/api/types.go. + +Examples: + $ kubectl get pods + + + $ kubectl get replicationController 1234-56-7890-234234-456456 + + + $ kubectl get -f json pod 1234-56-7890-234234-456456 + `, + Run: func(cmd *cobra.Command, args []string) { + var resource, id string + if len(args) == 0 { + usageError(cmd, "Need to supply a resource.") + } + if len(args) >= 1 { + resource = args[0] + } + if len(args) >= 2 { + id = args[1] + } + outputFormat := getFlagString(cmd, "output") + templateFile := getFlagString(cmd, "template") + selector := getFlagString(cmd, "selector") + err := kubectl.Get(out, getKubeClient(cmd).RESTClient, resource, id, selector, outputFormat, templateFile) + checkErr(err) + }, + } + // TODO Add an --output-version lock which can ensure that regardless of the + // server version, the client output stays the same. + cmd.Flags().StringP("output", "o", "console", "Output format: console|json|yaml|template") + cmd.Flags().StringP("template", "t", "", "Path to template file to use when --output=template") + cmd.Flags().StringP("selector", "l", "", "Selector (label query) to filter on") + return cmd +} diff --git a/pkg/kubectl/cmd/proxy.go b/pkg/kubectl/cmd/proxy.go new file mode 100644 index 00000000000..74897d53cfd --- /dev/null +++ b/pkg/kubectl/cmd/proxy.go @@ -0,0 +1,42 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/golang/glog" + "github.com/spf13/cobra" +) + +func NewCmdProxy(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "proxy", + Short: "Run a proxy to the Kubernetes API server", + Long: `Run a proxy to the Kubernetes API server.`, + Run: func(cmd *cobra.Command, args []string) { + port := getFlagInt(cmd, "port") + glog.Infof("Starting to serve on localhost:%d", port) + server := kubectl.NewProxyServer(getFlagString(cmd, "www"), getKubeClient(cmd), port) + glog.Fatal(server.Serve()) + }, + } + cmd.Flags().StringP("www", "w", "", "Also serve static files from the given directory under the prefix /static") + cmd.Flags().IntP("port", "p", 8001, "The port on which to run the proxy") + return cmd +} diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go new file mode 100644 index 00000000000..08907a9afc2 --- /dev/null +++ b/pkg/kubectl/cmd/update.go @@ -0,0 +1,55 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/spf13/cobra" +) + +func NewCmdUpdate(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "update -f filename", + Short: "Update a resource by filename or stdin", + Long: `Update a resource by filename or stdin. + +JSON and YAML formats are accepted. + +Examples: + $ kubectl update -f pod.json + + + $ cat pod.json | kubectl update -f - + `, + Run: func(cmd *cobra.Command, args []string) { + filename := getFlagString(cmd, "filename") + if len(filename) == 0 { + usageError(cmd, "Must pass a filename to update") + } + + data, err := readConfigData(filename) + checkErr(err) + + err = kubectl.Modify(out, getKubeClient(cmd).RESTClient, kubectl.ModifyUpdate, data) + checkErr(err) + }, + } + cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to update the resource") + return cmd +} diff --git a/pkg/kubectl/cmd/version.go b/pkg/kubectl/cmd/version.go new file mode 100644 index 00000000000..f0885808d79 --- /dev/null +++ b/pkg/kubectl/cmd/version.go @@ -0,0 +1,40 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/spf13/cobra" +) + +func NewCmdVersion(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Print version of client and server", + Run: func(cmd *cobra.Command, args []string) { + if getFlagBool(cmd, "client") { + kubectl.GetClientVersion(out) + } else { + kubectl.GetVersion(out, getKubeClient(cmd)) + } + }, + } + cmd.Flags().BoolP("client", "c", false, "Client version only (no server required)") + return cmd +} diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go new file mode 100644 index 00000000000..0387a87c2ee --- /dev/null +++ b/pkg/kubectl/describe.go @@ -0,0 +1,186 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "fmt" + "io" + "strings" + "text/tabwriter" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/golang/glog" +) + +func Describe(w io.Writer, c client.Interface, resource, id string) error { + var str string + var err error + path, err := resolveResource(resolveToPath, resource) + if err != nil { + return err + } + switch path { + case "pods": + str, err = describePod(w, c, id) + case "replicationControllers": + str, err = describeReplicationController(w, c, id) + case "services": + str, err = describeService(w, c, id) + case "minions": + str, err = describeMinion(w, c, id) + } + + if err != nil { + return err + } + + _, err = fmt.Fprintf(w, str) + return err +} + +func describePod(w io.Writer, c client.Interface, id string) (string, error) { + pod, err := c.GetPod(api.NewDefaultContext(), id) + if err != nil { + return "", err + } + + return tabbedString(func(out *tabwriter.Writer) error { + fmt.Fprintf(out, "ID:\t%s\n", pod.ID) + fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(pod.DesiredState.Manifest)) + fmt.Fprintf(out, "Host:\t%s\n", pod.CurrentState.Host+"/"+pod.CurrentState.HostIP) + fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(pod.Labels)) + fmt.Fprintf(out, "Status:\t%s\n", string(pod.CurrentState.Status)) + fmt.Fprintf(out, "Replication Controllers:\t%s\n", getReplicationControllersForLabels(c, labels.Set(pod.Labels))) + return nil + }) +} + +func describeReplicationController(w io.Writer, c client.Interface, id string) (string, error) { + rc, err := c.GetReplicationController(api.NewDefaultContext(), id) + if err != nil { + return "", err + } + + running, waiting, terminated, err := getPodStatusForReplicationController(c, rc) + if err != nil { + return "", err + } + + return tabbedString(func(out *tabwriter.Writer) error { + fmt.Fprintf(out, "ID:\t%s\n", rc.ID) + fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(rc.DesiredState.PodTemplate.DesiredState.Manifest)) + fmt.Fprintf(out, "Selector:\t%s\n", formatLabels(rc.DesiredState.ReplicaSelector)) + fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(rc.Labels)) + fmt.Fprintf(out, "Replicas:\t%d current / %d desired\n", rc.CurrentState.Replicas, rc.DesiredState.Replicas) + fmt.Fprintf(out, "Pods Status:\t%d Running / %d Waiting / %d Terminated\n", running, waiting, terminated) + return nil + }) +} + +func describeService(w io.Writer, c client.Interface, id string) (string, error) { + s, err := c.GetService(api.NewDefaultContext(), id) + if err != nil { + return "", err + } + + return tabbedString(func(out *tabwriter.Writer) error { + fmt.Fprintf(out, "ID:\t%s\n", s.ID) + fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(s.Labels)) + fmt.Fprintf(out, "Selector:\t%s\n", formatLabels(s.Selector)) + fmt.Fprintf(out, "Port:\t%d\n", s.Port) + return nil + }) +} + +func describeMinion(w io.Writer, c client.Interface, id string) (string, error) { + m, err := getMinion(c, id) + if err != nil { + return "", err + } + + return tabbedString(func(out *tabwriter.Writer) error { + fmt.Fprintf(out, "ID:\t%s\n", m.ID) + return nil + }) +} + +// client.Interface doesn't have GetMinion(id) yet so we hack it up. +func getMinion(c client.Interface, id string) (*api.Minion, error) { + minionList, err := c.ListMinions() + if err != nil { + glog.Fatalf("Error getting minion info: %v\n", err) + } + + for _, m := range minionList.Items { + if id == m.TypeMeta.ID { + return &m, nil + } + } + return nil, fmt.Errorf("Minion %s not found", id) +} + +// Get all replication controllers whose selectors would match a given set of +// labels. +// TODO Move this to pkg/client and ideally implement it server-side (instead +// of getting all RC's and searching through them manually). +func getReplicationControllersForLabels(c client.Interface, labelsToMatch labels.Labels) string { + // Get all replication controllers. + rcs, err := c.ListReplicationControllers(api.NewDefaultContext(), labels.Everything()) + if err != nil { + glog.Fatalf("Error getting replication controllers: %v\n", err) + } + + // Find the ones that match labelsToMatch. + var matchingRCs []api.ReplicationController + for _, rc := range rcs.Items { + selector := labels.SelectorFromSet(rc.DesiredState.ReplicaSelector) + if selector.Matches(labelsToMatch) { + matchingRCs = append(matchingRCs, rc) + } + } + + // Format the matching RC's into strings. + var rcStrings []string + for _, rc := range matchingRCs { + rcStrings = append(rcStrings, fmt.Sprintf("%s (%d/%d replicas created)", rc.ID, rc.CurrentState.Replicas, rc.DesiredState.Replicas)) + } + + list := strings.Join(rcStrings, ", ") + if list == "" { + return "" + } + return list +} + +func getPodStatusForReplicationController(kubeClient client.Interface, rc *api.ReplicationController) (running, waiting, terminated int, err error) { + rcPods, err := kubeClient.ListPods(api.NewDefaultContext(), labels.SelectorFromSet(rc.DesiredState.ReplicaSelector)) + if err != nil { + return + } + for _, pod := range rcPods.Items { + if pod.CurrentState.Status == api.PodRunning { + running++ + } else if pod.CurrentState.Status == api.PodWaiting { + waiting++ + } else if pod.CurrentState.Status == api.PodTerminated { + terminated++ + } + } + return +} diff --git a/pkg/kubectl/doc.go b/pkg/kubectl/doc.go new file mode 100644 index 00000000000..29c8c3c839b --- /dev/null +++ b/pkg/kubectl/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl is a set of libraries that are used by the kubectl command line tool. +// They are separated out into a library to support unit testing. Most functionality should +// be included in this package, and the main kubectl should really just be an entry point. +package kubectl diff --git a/pkg/kubectl/get.go b/pkg/kubectl/get.go new file mode 100644 index 00000000000..5a1adac9b01 --- /dev/null +++ b/pkg/kubectl/get.go @@ -0,0 +1,56 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func Get(w io.Writer, c *client.RESTClient, resource, id, selector, format, templateFile string) error { + path, err := resolveResource(resolveToPath, resource) + if err != nil { + return err + } + + r := c.Verb("GET").Path(path) + if len(id) > 0 { + r.Path(id) + } + if len(selector) > 0 { + r.ParseSelectorParam("labels", selector) + } + result := r.Do() + obj, err := result.Get() + if err != nil { + return err + } + + printer, err := getPrinter(format, templateFile) + if err != nil { + return err + } + + if err = printer.PrintObj(obj, w); err != nil { + body, _ := result.Raw() + return fmt.Errorf("Failed to print: %v\nRaw received object:\n%#v\n\nBody received: %v", err, obj, string(body)) + } + + return nil +} diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go new file mode 100644 index 00000000000..27b11886401 --- /dev/null +++ b/pkg/kubectl/kubectl.go @@ -0,0 +1,228 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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. +*/ + +// A set of common functions needed by cmd/kubectl and pkg/kubectl packages. +package kubectl + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "reflect" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/version" + "gopkg.in/v1/yaml" +) + +var apiVersionToUse = "v1beta1" + +func GetKubeClient(config *client.Config, matchVersion bool) (*client.Client, error) { + // TODO: get the namespace context when kubectl ns is completed + c, err := client.New(config) + if err != nil { + return nil, err + } + + if matchVersion { + clientVersion := version.Get() + serverVersion, err := c.ServerVersion() + if err != nil { + return nil, fmt.Errorf("Couldn't read version from server: %v\n", err) + } + if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) { + return nil, fmt.Errorf("Server version (%#v) differs from client version (%#v)!\n", s, clientVersion) + } + } + + return c, nil +} + +type AuthInfo struct { + User string + Password string + CAFile string + CertFile string + KeyFile string + Insecure *bool +} + +// LoadAuthInfo parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. +func LoadAuthInfo(path string, r io.Reader) (*AuthInfo, error) { + var auth AuthInfo + 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 + } + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + err = json.Unmarshal(data, &auth) + if err != nil { + return nil, err + } + return &auth, err +} + +func promptForString(field string, r io.Reader) string { + fmt.Printf("Please enter %s: ", field) + var result string + fmt.Fscan(r, &result) + return result +} + +func CreateResource(resource, id string) ([]byte, error) { + kind, err := resolveResource(resolveToKind, resource) + if err != nil { + return nil, err + } + + s := fmt.Sprintf(`{"kind": "%s", "apiVersion": "%s", "id": "%s"}`, kind, apiVersionToUse, id) + return []byte(s), nil +} + +// TODO Move to labels package. +func formatLabels(labelMap map[string]string) string { + l := labels.Set(labelMap).String() + if l == "" { + l = "" + } + return l +} + +func makeImageList(manifest api.ContainerManifest) string { + var images []string + for _, container := range manifest.Containers { + images = append(images, container.Image) + } + return strings.Join(images, ",") +} + +// Takes input 'data' as either json or yaml and attemps to decode it into the +// supplied object. +func dataToObject(data []byte) (runtime.Object, error) { + // This seems hacky but we can't get the codec from kubeClient. + versionInterfaces, err := latest.InterfacesFor(apiVersionToUse) + if err != nil { + return nil, err + } + obj, err := versionInterfaces.Codec.Decode(data) + if err != nil { + return nil, err + } + return obj, nil +} + +const ( + resolveToPath = "path" + resolveToKind = "kind" +) + +// Takes a human-friendly reference to a resource and converts it to either a +// resource path for an API call or to a Kind to construct a JSON definition. +// See usages of the function for more context. +// +// target is one of the above constants ("path" or "kind") to determine what to +// resolve the resource to. +// +// resource is the human-friendly reference to the resource you want to +// convert. +func resolveResource(target, resource string) (string, error) { + if target != resolveToPath && target != resolveToKind { + return "", fmt.Errorf("Unrecognized target to convert to: %s", target) + } + + var resolved string + var err error + // Caseless comparison. + resource = strings.ToLower(resource) + switch resource { + case "pods", "pod", "po": + if target == resolveToPath { + resolved = "pods" + } else { + resolved = "Pod" + } + case "replicationcontrollers", "replicationcontroller", "rc": + if target == resolveToPath { + resolved = "replicationControllers" + } else { + resolved = "ReplicationController" + } + case "services", "service", "se": + if target == resolveToPath { + resolved = "services" + } else { + resolved = "Service" + } + case "minions", "minion", "mi": + if target == resolveToPath { + resolved = "minions" + } else { + resolved = "Minion" + } + default: + // It might be a GUID, but we don't know how to handle those for now. + err = fmt.Errorf("Resource %s not recognized; need pods, replicationContollers, services or minions.", resource) + } + return resolved, err +} + +func resolveKindToResource(kind string) (resource string, err error) { + // Determine the REST resource according to the type in data. + switch kind { + case "Pod": + resource = "pods" + case "ReplicationController": + resource = "replicationControllers" + case "Service": + resource = "services" + default: + err = fmt.Errorf("Object %s not recognized", kind) + } + return +} + +// versionAndKind will return the APIVersion and Kind of the given wire-format +// enconding of an APIObject, or an error. This is hacked in until the +// migration to v1beta3. +func versionAndKind(data []byte) (version, kind string, err error) { + findKind := struct { + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + }{} + // yaml is a superset of json, so we use it to decode here. That way, + // we understand both. + err = yaml.Unmarshal(data, &findKind) + if err != nil { + return "", "", fmt.Errorf("couldn't get version/kind: %v", err) + } + return findKind.APIVersion, findKind.Kind, nil +} diff --git a/pkg/kubectl/kubectl_test.go b/pkg/kubectl/kubectl_test.go new file mode 100644 index 00000000000..5ce1f38317a --- /dev/null +++ b/pkg/kubectl/kubectl_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func validateAction(expectedAction, actualAction client.FakeAction, t *testing.T) { + if !reflect.DeepEqual(expectedAction, actualAction) { + t.Errorf("Unexpected Action: %#v, expected: %#v", actualAction, expectedAction) + } +} + +func TestLoadAuthInfo(t *testing.T) { + loadAuthInfoTests := []struct { + authData string + authInfo *AuthInfo + r io.Reader + }{ + { + `{"user": "user", "password": "pass"}`, + &AuthInfo{User: "user", Password: "pass"}, + nil, + }, + { + "", nil, nil, + }, + { + "missing", + &AuthInfo{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 := LoadAuthInfo(aifile.Name(), tt.r) + if len(tt.authData) == 0 && tt.authData != "missing" { + if err == nil { + t.Error("LoadAuthInfo 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) + } + } +} diff --git a/pkg/kubectl/modify.go b/pkg/kubectl/modify.go new file mode 100644 index 00000000000..dd3a96b40b1 --- /dev/null +++ b/pkg/kubectl/modify.go @@ -0,0 +1,162 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +type ModifyAction string + +const ( + ModifyCreate = ModifyAction("create") + ModifyUpdate = ModifyAction("update") + ModifyDelete = ModifyAction("delete") +) + +func Modify(w io.Writer, c *client.RESTClient, action ModifyAction, data []byte) error { + if action != ModifyCreate && action != ModifyUpdate && action != ModifyDelete { + return fmt.Errorf("Action not recognized") + } + + // TODO Support multiple API versions. + version, kind, err := versionAndKind(data) + if err != nil { + return err + } + + if version != apiVersionToUse { + return fmt.Errorf("Only supporting API version '%s' for now (version '%s' specified)", apiVersionToUse, version) + } + + obj, err := dataToObject(data) + if err != nil { + if err.Error() == "No type '' for version ''" { + return fmt.Errorf("Object could not be decoded. Make sure it has the Kind field defined.") + } + return err + } + + resource, err := resolveKindToResource(kind) + if err != nil { + return err + } + + var id string + switch action { + case "create": + id, err = doCreate(c, resource, data) + case "update": + id, err = doUpdate(c, resource, obj) + case "delete": + id, err = doDelete(c, resource, obj) + } + + if err != nil { + return err + } + + fmt.Fprintf(w, "%s\n", id) + return nil +} + +// Creates the object then returns the ID of the newly created object. +func doCreate(c *client.RESTClient, resource string, data []byte) (string, error) { + obj, err := c.Post().Path(resource).Body(data).Do().Get() + if err != nil { + return "", err + } + return getIDFromObj(obj) +} + +// Creates the object then returns the ID of the newly created object. +func doUpdate(c *client.RESTClient, resource string, obj runtime.Object) (string, error) { + // Figure out the ID of the object to update by introspecting into the + // object. + id, err := getIDFromObj(obj) + if err != nil { + return "", fmt.Errorf("ID not retrievable from object for update: %v", err) + } + + // Get the object from the server to find out its current resource + // version to prevent race conditions in updating the object. + serverObj, err := c.Get().Path(resource).Path(id).Do().Get() + if err != nil { + return "", fmt.Errorf("Item ID %s does not exist for update: %v", id, err) + } + version, err := getResourceVersionFromObj(serverObj) + if err != nil { + return "", err + } + + // Update the object we are trying to send to the server with the + // correct resource version. + typeMeta, err := runtime.FindTypeMeta(obj) + if err != nil { + return "", err + } + typeMeta.SetResourceVersion(version) + + // Convert object with updated resourceVersion to data for PUT. + data, err := c.Codec.Encode(obj) + if err != nil { + return "", err + } + + // Do the update. + err = c.Put().Path(resource).Path(id).Body(data).Do().Error() + fmt.Printf("r: %q, i: %q, d: %s", resource, id, data) + if err != nil { + return "", err + } + + return id, nil +} + +func doDelete(c *client.RESTClient, resource string, obj runtime.Object) (string, error) { + id, err := getIDFromObj(obj) + if err != nil { + return "", fmt.Errorf("ID not retrievable from object for update: %v", err) + } + + err = c.Delete().Path(resource).Path(id).Do().Error() + if err != nil { + return "", err + } + + return id, nil +} + +func getIDFromObj(obj runtime.Object) (string, error) { + typeMeta, err := runtime.FindTypeMeta(obj) + if err != nil { + return "", err + } + return typeMeta.ID(), nil +} + +func getResourceVersionFromObj(obj runtime.Object) (string, error) { + typeMeta, err := runtime.FindTypeMeta(obj) + if err != nil { + return "", err + } + return typeMeta.ResourceVersion(), nil +} diff --git a/pkg/kubectl/proxy_server.go b/pkg/kubectl/proxy_server.go new file mode 100644 index 00000000000..1a796ab71ac --- /dev/null +++ b/pkg/kubectl/proxy_server.go @@ -0,0 +1,89 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "fmt" + "net/http" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +// ProxyServer is a http.Handler which proxies Kubernetes APIs to remote API server. +type ProxyServer struct { + Client *client.Client + Port int +} + +func newFileHandler(prefix, base string) http.Handler { + return http.StripPrefix(prefix, http.FileServer(http.Dir(base))) +} + +// NewProxyServer creates and installs a new ProxyServer. +// It automatically registers the created ProxyServer to http.DefaultServeMux. +func NewProxyServer(filebase string, kubeClient *client.Client, port int) *ProxyServer { + server := &ProxyServer{ + Client: kubeClient, + Port: port, + } + http.Handle("/api/", server) + http.Handle("/static/", newFileHandler("/static/", filebase)) + return server +} + +// Serve starts the server (http.DefaultServeMux) on TCP port 8001, loops forever. +func (s *ProxyServer) Serve() error { + addr := fmt.Sprintf(":%d", s.Port) + return http.ListenAndServe(addr, nil) +} + +func (s *ProxyServer) doError(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusInternalServerError) + w.Header().Add("Content-type", "application/json") + data, _ := latest.Codec.Encode(&api.Status{ + Status: api.StatusFailure, + Message: fmt.Sprintf("internal error: %#v", err), + }) + w.Write(data) +} + +func (s *ProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + url := r.URL + selector := url.Query().Get("labels") + fieldSelector := url.Query().Get("fields") + result := s.Client. + Verb(r.Method). + AbsPath(r.URL.Path). + ParseSelectorParam("labels", selector). + ParseSelectorParam("fields", fieldSelector). + Body(r.Body). + Do() + if result.Error() != nil { + s.doError(w, result.Error()) + return + } + data, err := result.Raw() + if err != nil { + s.doError(w, err) + return + } + w.Header().Add("Content-type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(data) +} diff --git a/pkg/kubectl/proxy_server_test.go b/pkg/kubectl/proxy_server_test.go new file mode 100644 index 00000000000..15cd89e31c9 --- /dev/null +++ b/pkg/kubectl/proxy_server_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func TestFileServing(t *testing.T) { + data := "This is test data" + dir, err := ioutil.TempDir("", "data") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + err = ioutil.WriteFile(dir+"/test.txt", []byte(data), 0755) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + prefix := "/foo/" + handler := newFileHandler(prefix, dir) + server := httptest.NewServer(handler) + client := http.Client{} + req, err := http.NewRequest("GET", server.URL+prefix+"test.txt", nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + res, err := client.Do(req) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + defer res.Body.Close() + b, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("Unexpected status: %d", res.StatusCode) + } + if string(b) != data { + t.Errorf("Data doesn't match: %s vs %s", string(b), data) + } +} diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go new file mode 100644 index 00000000000..c8652913152 --- /dev/null +++ b/pkg/kubectl/resource_printer.go @@ -0,0 +1,300 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "reflect" + "strings" + "text/tabwriter" + "text/template" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/golang/glog" + "gopkg.in/v1/yaml" +) + +func getPrinter(format, templateFile string) (ResourcePrinter, error) { + var printer ResourcePrinter + switch format { + case "json": + printer = &JSONPrinter{} + case "yaml": + printer = &YAMLPrinter{} + case "template": + var data []byte + if len(templateFile) > 0 { + var err error + data, err = ioutil.ReadFile(templateFile) + if err != nil { + return printer, fmt.Errorf("Error reading template %s, %v\n", templateFile, err) + } + } else { + return printer, fmt.Errorf("template format specified but no template file given") + } + tmpl, err := template.New("output").Parse(string(data)) + if err != nil { + return printer, fmt.Errorf("Error parsing template %s, %v\n", string(data), err) + } + printer = &TemplatePrinter{ + Template: tmpl, + } + default: + printer = NewHumanReadablePrinter() + } + return printer, nil +} + +// ResourcePrinter is an interface that knows how to print API resources. +type ResourcePrinter interface { + // Print receives an arbitrary JSON body, formats it and prints it to a writer. + PrintObj(runtime.Object, io.Writer) error +} + +// IdentityPrinter is an implementation of ResourcePrinter which simply copies the body out to the output stream. +type JSONPrinter struct{} + +// PrintObj is an implementation of ResourcePrinter.PrintObj which simply writes the object to the Writer. +func (i *JSONPrinter) PrintObj(obj runtime.Object, w io.Writer) error { + output, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprint(w, string(output)+"\n") + return err +} + +// YAMLPrinter is an implementation of ResourcePrinter which parsess JSON, and re-formats as YAML. +type YAMLPrinter struct{} + +// PrintObj prints the data as YAML. +func (y *YAMLPrinter) PrintObj(obj runtime.Object, w io.Writer) error { + output, err := yaml.Marshal(obj) + if err != nil { + return err + } + _, err = fmt.Fprint(w, string(output)) + return err +} + +type handlerEntry struct { + columns []string + printFunc reflect.Value +} + +// HumanReadablePrinter is an implementation of ResourcePrinter which attempts to provide more elegant output. +type HumanReadablePrinter struct { + handlerMap map[reflect.Type]*handlerEntry +} + +// NewHumanReadablePrinter creates a HumanReadablePrinter. +func NewHumanReadablePrinter() *HumanReadablePrinter { + printer := &HumanReadablePrinter{make(map[reflect.Type]*handlerEntry)} + printer.addDefaultHandlers() + return printer +} + +// Handler adds a print handler with a given set of columns to HumanReadablePrinter instance. +// printFunc is the function that will be called to print an object. +// It must be of the following type: +// func printFunc(object ObjectType, w io.Writer) error +// where ObjectType is the type of the object that will be printed. +func (h *HumanReadablePrinter) Handler(columns []string, printFunc interface{}) error { + printFuncValue := reflect.ValueOf(printFunc) + if err := h.validatePrintHandlerFunc(printFuncValue); err != nil { + glog.Errorf("Unable to add print handler: %v", err) + return err + } + objType := printFuncValue.Type().In(0) + h.handlerMap[objType] = &handlerEntry{ + columns: columns, + printFunc: printFuncValue, + } + return nil +} + +func (h *HumanReadablePrinter) validatePrintHandlerFunc(printFunc reflect.Value) error { + if printFunc.Kind() != reflect.Func { + return fmt.Errorf("Invalid print handler. %#v is not a function.", printFunc) + } + funcType := printFunc.Type() + if funcType.NumIn() != 2 || funcType.NumOut() != 1 { + return fmt.Errorf("Invalid print handler." + + "Must accept 2 parameters and return 1 value.") + } + if funcType.In(1) != reflect.TypeOf((*io.Writer)(nil)).Elem() || + funcType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { + return fmt.Errorf("Invalid print handler. The expected signature is: "+ + "func handler(obj %v, w io.Writer) error", funcType.In(0)) + } + return nil +} + +var podColumns = []string{"ID", "IMAGE(S)", "HOST", "LABELS", "STATUS"} +var replicationControllerColumns = []string{"ID", "IMAGE(S)", "SELECTOR", "REPLICAS"} +var serviceColumns = []string{"ID", "LABELS", "SELECTOR", "PORT"} +var minionColumns = []string{"ID"} +var statusColumns = []string{"STATUS"} + +// addDefaultHandlers adds print handlers for default Kubernetes types. +func (h *HumanReadablePrinter) addDefaultHandlers() { + h.Handler(podColumns, printPod) + h.Handler(podColumns, printPodList) + h.Handler(replicationControllerColumns, printReplicationController) + h.Handler(replicationControllerColumns, printReplicationControllerList) + h.Handler(serviceColumns, printService) + h.Handler(serviceColumns, printServiceList) + h.Handler(minionColumns, printMinion) + h.Handler(minionColumns, printMinionList) + h.Handler(statusColumns, printStatus) +} + +func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error { + _, err := fmt.Fprintf(w, "Unknown object: %s", string(data)) + return err +} + +func (h *HumanReadablePrinter) printHeader(columnNames []string, w io.Writer) error { + if _, err := fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")); err != nil { + return err + } + return nil +} + +func podHostString(host, ip string) string { + if host == "" && ip == "" { + return "" + } + return host + "/" + ip +} + +func printPod(pod *api.Pod, w io.Writer) error { + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", + pod.ID, makeImageList(pod.DesiredState.Manifest), + podHostString(pod.CurrentState.Host, pod.CurrentState.HostIP), + labels.Set(pod.Labels), pod.CurrentState.Status) + return err +} + +func printPodList(podList *api.PodList, w io.Writer) error { + for _, pod := range podList.Items { + if err := printPod(&pod, w); err != nil { + return err + } + } + return nil +} + +func printReplicationController(ctrl *api.ReplicationController, w io.Writer) error { + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", + ctrl.ID, makeImageList(ctrl.DesiredState.PodTemplate.DesiredState.Manifest), + labels.Set(ctrl.DesiredState.ReplicaSelector), ctrl.DesiredState.Replicas) + return err +} + +func printReplicationControllerList(list *api.ReplicationControllerList, w io.Writer) error { + for _, ctrl := range list.Items { + if err := printReplicationController(&ctrl, w); err != nil { + return err + } + } + return nil +} + +func printService(svc *api.Service, w io.Writer) error { + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", svc.ID, labels.Set(svc.Labels), + labels.Set(svc.Selector), svc.Port) + return err +} + +func printServiceList(list *api.ServiceList, w io.Writer) error { + for _, svc := range list.Items { + if err := printService(&svc, w); err != nil { + return err + } + } + return nil +} + +func printMinion(minion *api.Minion, w io.Writer) error { + _, err := fmt.Fprintf(w, "%s\n", minion.ID) + return err +} + +func printMinionList(list *api.MinionList, w io.Writer) error { + for _, minion := range list.Items { + if err := printMinion(&minion, w); err != nil { + return err + } + } + return nil +} + +func printStatus(status *api.Status, w io.Writer) error { + _, err := fmt.Fprintf(w, "%v\n", status.Status) + return err +} + +// PrintObj prints the obj in a human-friendly format according to the type of the obj. +func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error { + w := tabwriter.NewWriter(output, 20, 5, 3, ' ', 0) + defer w.Flush() + if handler := h.handlerMap[reflect.TypeOf(obj)]; handler != nil { + h.printHeader(handler.columns, w) + args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(w)} + resultValue := handler.printFunc.Call(args)[0] + if resultValue.IsNil() { + return nil + } else { + return resultValue.Interface().(error) + } + } else { + return fmt.Errorf("Error: unknown type %#v", obj) + } +} + +// TemplatePrinter is an implementation of ResourcePrinter which formats data with a Go Template. +type TemplatePrinter struct { + Template *template.Template +} + +// PrintObj formats the obj with the Go Template. +func (t *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { + return t.Template.Execute(w, obj) +} + +func tabbedString(f func(*tabwriter.Writer) error) (string, error) { + out := new(tabwriter.Writer) + b := make([]byte, 1024) + buf := bytes.NewBuffer(b) + out.Init(buf, 0, 8, 1, '\t', 0) + + err := f(out) + if err != nil { + return "", err + } + + out.Flush() + str := string(buf.String()) + return str, nil +} diff --git a/pkg/kubectl/resource_printer_test.go b/pkg/kubectl/resource_printer_test.go new file mode 100644 index 00000000000..2ee7c742922 --- /dev/null +++ b/pkg/kubectl/resource_printer_test.go @@ -0,0 +1,141 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "gopkg.in/v1/yaml" +) + +type testStruct struct { + Key string `yaml:"Key" json:"Key"` + Map map[string]int `yaml:"Map" json:"Map"` + StringList []string `yaml:"StringList" json:"StringList"` + IntList []int `yaml:"IntList" json:"IntList"` +} + +func (ts *testStruct) IsAnAPIObject() {} + +var testData = testStruct{ + "testValue", + map[string]int{"TestSubkey": 1}, + []string{"a", "b", "c"}, + []int{1, 2, 3}, +} + +func TestYAMLPrinter(t *testing.T) { + testPrinter(t, &YAMLPrinter{}, yaml.Unmarshal) +} + +func TestJSONPrinter(t *testing.T) { + testPrinter(t, &JSONPrinter{}, json.Unmarshal) +} + +func testPrinter(t *testing.T, printer ResourcePrinter, unmarshalFunc func(data []byte, v interface{}) error) { + buf := bytes.NewBuffer([]byte{}) + + err := printer.PrintObj(&testData, buf) + if err != nil { + t.Fatal(err) + } + var poutput testStruct + err = yaml.Unmarshal(buf.Bytes(), &poutput) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(testData, poutput) { + t.Errorf("Test data and unmarshaled data are not equal: %#v vs %#v", poutput, testData) + } + + obj := &api.Pod{ + TypeMeta: api.TypeMeta{ID: "foo"}, + } + buf.Reset() + printer.PrintObj(obj, buf) + var objOut api.Pod + err = yaml.Unmarshal([]byte(buf.String()), &objOut) + if err != nil { + t.Errorf("Unexpeted error: %#v", err) + } + if !reflect.DeepEqual(obj, &objOut) { + t.Errorf("Unexpected inequality: %#v vs %#v", obj, &objOut) + } +} + +type TestPrintType struct { + Data string +} + +func (*TestPrintType) IsAnAPIObject() {} + +type TestUnknownType struct{} + +func (*TestUnknownType) IsAnAPIObject() {} + +func PrintCustomType(obj *TestPrintType, w io.Writer) error { + _, err := fmt.Fprintf(w, "%s", obj.Data) + return err +} + +func ErrorPrintHandler(obj *TestPrintType, w io.Writer) error { + return fmt.Errorf("ErrorPrintHandler error") +} + +func TestCustomTypePrinting(t *testing.T) { + columns := []string{"Data"} + printer := NewHumanReadablePrinter() + printer.Handler(columns, PrintCustomType) + + obj := TestPrintType{"test object"} + buffer := &bytes.Buffer{} + err := printer.PrintObj(&obj, buffer) + if err != nil { + t.Errorf("An error occurred printing the custom type: %#v", err) + } + expectedOutput := "Data\ntest object" + if buffer.String() != expectedOutput { + t.Errorf("The data was not printed as expected. Expected:\n%s\nGot:\n%s", expectedOutput, buffer.String()) + } +} + +func TestPrintHandlerError(t *testing.T) { + columns := []string{"Data"} + printer := NewHumanReadablePrinter() + printer.Handler(columns, ErrorPrintHandler) + obj := TestPrintType{"test object"} + buffer := &bytes.Buffer{} + err := printer.PrintObj(&obj, buffer) + if err == nil || err.Error() != "ErrorPrintHandler error" { + t.Errorf("Did not get the expected error: %#v", err) + } +} + +func TestUnknownTypePrinting(t *testing.T) { + printer := NewHumanReadablePrinter() + buffer := &bytes.Buffer{} + err := printer.PrintObj(&TestUnknownType{}, buffer) + if err == nil { + t.Errorf("An error was expected from printing unknown type") + } +} diff --git a/pkg/kubectl/version.go b/pkg/kubectl/version.go new file mode 100644 index 00000000000..5c4c8e45cee --- /dev/null +++ b/pkg/kubectl/version.go @@ -0,0 +1,41 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 kubectl + +import ( + "fmt" + "io" + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/version" +) + +func GetVersion(w io.Writer, kubeClient client.Interface) { + serverVersion, err := kubeClient.ServerVersion() + if err != nil { + fmt.Printf("Couldn't read version from server: %v\n", err) + os.Exit(1) + } + + GetClientVersion(w) + fmt.Fprintf(w, "Server Version: %#v\n", serverVersion) +} + +func GetClientVersion(w io.Writer) { + fmt.Fprintf(w, "Client Version: %#v\n", version.Get()) +}