From 3b8488028df37ae39349f089dabfc56e1821c840 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Fri, 25 Jul 2014 12:28:20 -0700 Subject: [PATCH] Add /version to server and check it in client. Will help detect client/version skew and prevent e2e test from passing while running a version other than the one you think it's running. --- cmd/kubecfg/kubecfg.go | 65 +++++++++++++++++++++++++------------ hack/e2e-test.sh | 2 +- hack/test-cmd.sh | 4 +-- pkg/apiserver/apiserver.go | 22 ++++++++++++- pkg/client/client.go | 16 +++++++++ pkg/client/client_test.go | 29 +++++++++++++++++ pkg/kubecfg/kubecfg.go | 14 ++++++++ pkg/kubecfg/kubecfg_test.go | 6 ++-- pkg/version/version.go | 19 +++++++++-- 9 files changed, 147 insertions(+), 30 deletions(-) diff --git a/cmd/kubecfg/kubecfg.go b/cmd/kubecfg/kubecfg.go index bc20466beb0e9..a9d2609bc3c5e 100644 --- a/cmd/kubecfg/kubecfg.go +++ b/cmd/kubecfg/kubecfg.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "net/url" "os" + "reflect" "sort" "strconv" "strings" @@ -31,28 +32,28 @@ import ( kube_client "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubecfg" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/golang/glog" ) -// AppVersion is the current version of kubecfg. -const AppVersion = "0.1" - var ( - versionFlag = flag.Bool("V", false, "Print the version number.") - httpServer = flag.String("h", "", "The host to connect to.") - config = flag.String("c", "", "Path to the config file.") - selector = flag.String("l", "", "Selector (label query) to use for listing") - updatePeriod = flag.Duration("u", 60*time.Second, "Update interval period") - portSpec = flag.String("p", "", "The port spec, comma-separated list of :,...") - servicePort = flag.Int("s", -1, "If positive, create and run a corresponding service on this port, only used with 'run'") - authConfig = flag.String("auth", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user. Only used if doing https.") - json = flag.Bool("json", false, "If true, print raw JSON for responses") - yaml = flag.Bool("yaml", false, "If true, print raw YAML for responses") - verbose = flag.Bool("verbose", false, "If true, print extra information") - proxy = flag.Bool("proxy", false, "If true, run a proxy to the api server") - www = flag.String("www", "", "If -proxy is true, use this directory to serve static files") - templateFile = flag.String("template_file", "", "If present, load this file as a golang template and use it for output printing") - templateStr = flag.String("template", "", "If present, parse this string as a golang template and use it for output printing") + versionFlag = flag.Bool("V", false, "Print the version number.") + serverVersion = flag.Bool("server_version", false, "Print the server's version number.") + preventSkew = flag.Bool("expect_version_match", false, "Fail if server's version doesn't match own version.") + httpServer = flag.String("h", "", "The host to connect to.") + config = flag.String("c", "", "Path to the config file.") + selector = flag.String("l", "", "Selector (label query) to use for listing") + updatePeriod = flag.Duration("u", 60*time.Second, "Update interval period") + portSpec = flag.String("p", "", "The port spec, comma-separated list of :,...") + servicePort = flag.Int("s", -1, "If positive, create and run a corresponding service on this port, only used with 'run'") + authConfig = flag.String("auth", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user. Only used if doing https.") + json = flag.Bool("json", false, "If true, print raw JSON for responses") + yaml = flag.Bool("yaml", false, "If true, print raw YAML for responses") + verbose = flag.Bool("verbose", false, "If true, print extra information") + proxy = flag.Bool("proxy", false, "If true, run a proxy to the api server") + www = flag.String("www", "", "If -proxy is true, use this directory to serve static files") + templateFile = flag.String("template_file", "", "If present, load this file as a golang template and use it for output printing") + templateStr = flag.String("template", "", "If present, parse this string as a golang template and use it for output printing") ) func usage() { @@ -107,7 +108,7 @@ func main() { defer util.FlushLogs() if *versionFlag { - fmt.Println("Version:", AppVersion) + fmt.Printf("Version: %#v\n", version.Get()) os.Exit(0) } @@ -136,6 +137,30 @@ func main() { } } + client := kube_client.New(masterServer, auth) + + if *serverVersion { + got, err := client.ServerVersion() + if err != nil { + fmt.Printf("Couldn't read version from server: %v\n", err) + os.Exit(1) + } + fmt.Printf("Server Version: %#v\n", got) + os.Exit(0) + } + + if *preventSkew { + got, err := client.ServerVersion() + if err != nil { + fmt.Printf("Couldn't read version from server: %v\n", err) + os.Exit(1) + } + if c, s := version.Get(), *got; !reflect.DeepEqual(c, s) { + fmt.Printf("Server version (%#v) differs from client version (%#v)!\n", s, c) + os.Exit(1) + } + } + if *proxy { glog.Info("Starting to serve on localhost:8001") server := kubecfg.NewProxyServer(*www, masterServer, auth) @@ -148,8 +173,6 @@ func main() { } method := flag.Arg(0) - client := kube_client.New(masterServer, auth) - matchFound := executeAPIRequest(method, client) || executeControllerRequest(method, client) if matchFound == false { glog.Fatalf("Unknown command %s", method) diff --git a/hack/e2e-test.sh b/hack/e2e-test.sh index 3d8aa18baf0a3..d61a81c756098 100755 --- a/hack/e2e-test.sh +++ b/hack/e2e-test.sh @@ -39,7 +39,7 @@ set -e # Use testing config export KUBE_CONFIG_FILE="config-test.sh" export KUBE_REPO_ROOT="$(dirname $0)/.." -export CLOUDCFG="${KUBE_REPO_ROOT}/cluster/kubecfg.sh" +export CLOUDCFG="${KUBE_REPO_ROOT}/cluster/kubecfg.sh -expect_version_match" # Build a release required by the test provider [if any] test-build-release diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index e4de8f8217b18..e3610a6ad2647 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -106,7 +106,7 @@ 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}" +KUBE_CMD="${GO_OUT}/kubecfg -h http://127.0.0.1:${API_PORT} -expect_version_match" ${KUBE_CMD} list pods echo "kubecfg(pods): ok" @@ -130,4 +130,4 @@ echo "kubecfg(minions): ok" #PROXY_LOG=/tmp/kube-proxy.log #${GO_OUT}/proxy \ # --etcd_servers="http://127.0.0.1:${ETCD_PORT}" 1>&2 & -#PROXY_PID=$! \ No newline at end of file +#PROXY_PID=$! diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 4de04026e22ef..c9f2ffffd2957 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -39,6 +39,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/golang/glog" ) @@ -151,6 +152,7 @@ func New(storage map[string]RESTStorage, prefix string) *APIServer { healthz.InstallHandler(s.mux) s.mux.HandleFunc("/", s.handleIndex) + s.mux.HandleFunc("/version", s.handleVersionReq) // Handle both operations and operations/* with the same handler s.mux.HandleFunc(s.operationPrefix(), s.handleOperationRequest) @@ -182,6 +184,11 @@ func (server *APIServer) handleIndex(w http.ResponseWriter, req *http.Request) { fmt.Fprint(w, data) } +// handleVersionReq writes the server's version information. +func (server *APIServer) handleVersionReq(w http.ResponseWriter, req *http.Request) { + server.writeRawJSON(http.StatusOK, version.Get(), w) +} + func (server *APIServer) handleMinionReq(w http.ResponseWriter, req *http.Request) { minionPrefix := "/proxy/minion/" if !strings.HasPrefix(req.URL.Path, minionPrefix) { @@ -344,14 +351,27 @@ func (server *APIServer) notFound(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "Not Found: %#v", req) } +// write writes an API object in wire format. func (server *APIServer) write(statusCode int, object interface{}, w http.ResponseWriter) { + output, err := api.Encode(object) + if err != nil { + server.error(err, w) + return + } w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) - output, err := api.Encode(object) + w.Write(output) +} + +// writeRawJSON writes a non-API object in JSON. +func (server *APIServer) writeRawJSON(statusCode int, object interface{}, w http.ResponseWriter) { + output, err := json.Marshal(object) if err != nil { server.error(err, w) return } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) w.Write(output) } diff --git a/pkg/client/client.go b/pkg/client/client.go index 759efb36a6288..98c5e5d6bba75 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -18,6 +18,7 @@ package client import ( "crypto/tls" + "encoding/json" "fmt" "io" "io/ioutil" @@ -26,6 +27,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/golang/glog" ) @@ -238,3 +240,17 @@ func (c *Client) UpdateService(svc api.Service) (result api.Service, err error) func (c *Client) DeleteService(name string) error { return c.Delete().Path("services").Path(name).Do().Error() } + +// ServerVersion retrieves and parses the server's version. +func (c *Client) ServerVersion() (*version.Info, error) { + body, err := c.Get().AbsPath("/version").Do().Raw() + if err != nil { + return nil, err + } + var info version.Info + err = json.Unmarshal(body, &info) + if err != nil { + return nil, fmt.Errorf("Got '%s': %v", string(body), err) + } + return &info, nil +} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index dc34ab36e2544..93c400c5de3cc 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -17,6 +17,7 @@ limitations under the License. package client import ( + "encoding/json" "net/http" "net/http/httptest" "net/url" @@ -26,6 +27,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/version" ) // TODO: Move this to a common place, it's needed in multiple tests. @@ -456,3 +458,30 @@ func TestDoRequestAcceptedSuccess(t *testing.T) { } fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil) } + +func TestGetServerVersion(t *testing.T) { + expect := version.Info{ + Major: "foo", + Minor: "bar", + GitCommit: "baz", + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + output, err := json.Marshal(expect) + if err != nil { + t.Errorf("unexpected encoding error: %v", err) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(output) + })) + client := New(server.URL, nil) + + got, err := client.ServerVersion() + if err != nil { + t.Fatalf("unexpected encoding error: %v", err) + } + if e, a := expect, *got; !reflect.DeepEqual(e, a) { + t.Errorf("expected %v, got %v", e, a) + } +} diff --git a/pkg/kubecfg/kubecfg.go b/pkg/kubecfg/kubecfg.go index 2b340cb1c5d29..88fa3cbd23aec 100644 --- a/pkg/kubecfg/kubecfg.go +++ b/pkg/kubecfg/kubecfg.go @@ -29,10 +29,24 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/golang/glog" "gopkg.in/v1/yaml" ) +func GetServerVersion(client *client.Client) (*version.Info, error) { + body, err := client.Get().AbsPath("/version").Do().Raw() + if err != nil { + return nil, err + } + var info version.Info + err = json.Unmarshal(body, &info) + if err != nil { + return nil, fmt.Errorf("Got '%s': %v", string(body), err) + } + return &info, nil +} + func promptForString(field string, r io.Reader) string { fmt.Printf("Please enter %s: ", field) var result string diff --git a/pkg/kubecfg/kubecfg_test.go b/pkg/kubecfg/kubecfg_test.go index 029d9d23d0414..34c519bfba213 100644 --- a/pkg/kubecfg/kubecfg_test.go +++ b/pkg/kubecfg/kubecfg_test.go @@ -317,9 +317,9 @@ func TestMakePorts(t *testing.T) { { "8080:80,8081:8081,443:444", []api.Port{ - api.Port{HostPort: 8080, ContainerPort: 80}, - api.Port{HostPort: 8081, ContainerPort: 8081}, - api.Port{HostPort: 443, ContainerPort: 444}, + {HostPort: 8080, ContainerPort: 80}, + {HostPort: 8081, ContainerPort: 8081}, + {HostPort: 443, ContainerPort: 444}, }, }, } diff --git a/pkg/version/version.go b/pkg/version/version.go index ea5e4db15eb3b..48f72bd53576e 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -16,6 +16,21 @@ limitations under the License. package version -func Get() (major, minor, gitCommit string) { - return "v1beta", "1", commitFromGit +// Info contains versioning information. +// TODO: Add []string of api versions supported? It's still unclear +// how we'll want to distribute that information. +type Info struct { + Major string `json:"major" yaml:"major"` + Minor string `json:"minor" yaml:"minor"` + GitCommit string `json:"gitCommit" yaml:"gitCommit"` +} + +// Get returns the overall codebase version. It's for detecting +// what code a binary was built from. +func Get() Info { + return Info{ + Major: "0", + Minor: "1", + GitCommit: commitFromGit, + } }