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.
pull/6/head
Daniel Smith 2014-07-25 12:28:20 -07:00
parent 9fc52c8aaa
commit 3b8488028d
9 changed files with 148 additions and 31 deletions

View File

@ -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 <external>:<internal>,...")
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 <external>:<internal>,...")
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)

View File

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

View File

@ -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=$!
#PROXY_PID=$!

View File

@ -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) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
output, err := api.Encode(object)
if err != nil {
server.error(err, w)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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