kubectl: kubecfg rewrite for better modularity and improved UX

pull/6/head
Sam Ghods 2014-10-05 18:24:19 -07:00
parent 0b79438237
commit 4b220f8b0a
22 changed files with 2052 additions and 13 deletions

27
cmd/kubectl/kubectl.go Normal file
View File

@ -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)
}

View File

@ -160,6 +160,7 @@ kube::default_build_targets() {
echo "cmd/e2e"
echo "cmd/kubelet"
echo "cmd/kubecfg"
echo "cmd/kubectl"
echo "plugin/cmd/scheduler"
}

View File

@ -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 \

255
pkg/kubectl/cmd/cmd.go Normal file
View File

@ -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
}
}

54
pkg/kubectl/cmd/create.go Normal file
View File

@ -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
<create a pod using the data in pod.json>
$ cat pod.json | kubectl create -f -
<create a pod based on the json passed into stdin>`,
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
}

80
pkg/kubectl/cmd/delete.go Normal file
View File

@ -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] | (<resource> <id>))",
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
<delete a pod using the type and id pod.json>
$ cat pod.json | kubectl delete -f -
<delete a pod based on the type and id in the json passed into stdin>
$ kubectl delete pod 1234-56-7890-234234-456456
<delete a pod with ID 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
}

View File

@ -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 <resource> <id>",
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
}

70
pkg/kubectl/cmd/get.go Normal file
View File

@ -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 <file>|--template=<file>] <resource> [<id>]",
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
<list all pods in ps output format>
$ kubectl get replicationController 1234-56-7890-234234-456456
<list single repliaction controller in ps output format>
$ kubectl get -f json pod 1234-56-7890-234234-456456
<list single pod in json output format>`,
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
}

42
pkg/kubectl/cmd/proxy.go Normal file
View File

@ -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
}

55
pkg/kubectl/cmd/update.go Normal file
View File

@ -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
<update a pod using the data in pod.json>
$ cat pod.json | kubectl update -f -
<update a pod based on the json passed into stdin>`,
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
}

View File

@ -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
}

186
pkg/kubectl/describe.go Normal file
View File

@ -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 "<none>"
}
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
}

20
pkg/kubectl/doc.go Normal file
View File

@ -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

56
pkg/kubectl/get.go Normal file
View File

@ -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
}

228
pkg/kubectl/kubectl.go Normal file
View File

@ -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 = "<none>"
}
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
}

View File

@ -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)
}
}
}

162
pkg/kubectl/modify.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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 "<unassigned>"
}
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
}

View File

@ -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")
}
}

41
pkg/kubectl/version.go Normal file
View File

@ -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())
}