diff --git a/.travis.yml b/.travis.yml index 78c78f8f5633..9b6736e6f29a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,6 @@ matrix: env: TEST_ASSETS=true install: - - ./hack/verify-gofmt.sh - ./hack/verify-jsonformat.sh - ./hack/install-etcd.sh - ./hack/install-std-race.sh @@ -23,9 +22,7 @@ install: - ./hack/install-assets.sh script: - - KUBE_RACE="-race" KUBE_COVER="-cover -covermode=atomic" ./hack/test-go.sh "" -p=4 - - ./hack/test-cmd.sh - - PATH=$HOME/gopath/bin:./_output/etcd/bin:$PATH ./hack/test-integration.sh + - PATH=$HOME/gopath/bin:./_output/etcd/bin:$PATH make check-test GOFLAGS="-p=4" - ./hack/test-assets.sh notifications: diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index adfc28d59c50..8fda4d5bcce3 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -5,11 +5,21 @@ "./..." ], "Deps": [ + { + "ImportPath": "bitbucket.org/ww/goautoneg", + "Comment": "null-5", + "Rev": "75cd24fc2f2c2a2088577d12123ddee5f54e0675" + }, { "ImportPath": "code.google.com/p/go-uuid/uuid", "Comment": "null-12", "Rev": "7dda39b2e7d5e265014674c5af696ba4186679e9" }, + { + "ImportPath": "code.google.com/p/go.exp/inotify", + "Comment": "null-75", + "Rev": "bd8df7009305d6ada223ea3c95b94c0f38bfa119" + }, { "ImportPath": "code.google.com/p/go.net/context", "Comment": "null-240", @@ -55,273 +65,278 @@ }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/admission", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/api", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/client", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" - }, - { - "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/constraint", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/controller", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/fields", + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/labels", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/master", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/probe", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/proxy", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/controller", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/endpoint", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/etcd", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/event", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/limitrange", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/minion", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" - }, - { - "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/scheduler", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/service", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/tools", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/types", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/ui", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/util", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/version", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/watch", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/token/tokenfile", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory", + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/algorithmprovider", + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/third_party/golang/netutil", - "Comment": "v0.11.0-330-g6241a21", - "Rev": "6241a211c8f35a6147aa3a0236f680ffa8e11037" + "Comment": "v0.12.0-992-gc5f7351", + "Rev": "c5f73516b677434d9cce7d07e460bb712c85e00b" }, { "ImportPath": "github.com/RangelReale/osin", @@ -336,6 +351,14 @@ "Comment": "v0.6.6-2-g2cea0f0", "Rev": "2cea0f0d141f56fae06df5b813ec4119d1c8ccbd" }, + { + "ImportPath": "github.com/abbot/go-http-auth", + "Rev": "c0ef4539dfab4d21c8ef20ba2924f9fc6f186d35" + }, + { + "ImportPath": "github.com/beorn7/perks/quantile", + "Rev": "b965b613227fddccbfffe13eae360ed3fa822f8d" + }, { "ImportPath": "github.com/coreos/etcd/config", "Comment": "v0.4.3-7-gfc2afe1", @@ -463,12 +486,18 @@ }, { "ImportPath": "github.com/coreos/go-systemd/activation", - "Comment": "v2-43-g2d21675", - "Rev": "2d21675230a81a503f4363f4aa3490af06d52bb8" + "Comment": "v2-27-g97e243d", + "Rev": "97e243d21a8e232e9d8af38ba2366dfcfceebeba" }, { "ImportPath": "github.com/coreos/go-systemd/daemon", - "Rev": "2d21675230a81a503f4363f4aa3490af06d52bb8" + "Comment": "v2-27-g97e243d", + "Rev": "97e243d21a8e232e9d8af38ba2366dfcfceebeba" + }, + { + "ImportPath": "github.com/coreos/go-systemd/dbus", + "Comment": "v2-27-g97e243d", + "Rev": "97e243d21a8e232e9d8af38ba2366dfcfceebeba" }, { "ImportPath": "github.com/davecgh/go-spew/spew", @@ -481,76 +510,82 @@ }, { "ImportPath": "github.com/docker/docker/builder/command", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/builder/parser", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/pkg/archive", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/pkg/fileutils", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/pkg/ioutils", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" + }, + { + "ImportPath": "github.com/docker/docker/pkg/mount", + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/pkg/parsers", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/pkg/pools", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/pkg/promise", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { - "ImportPath": "github.com/docker/docker/pkg/system", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "ImportPath": "github.com/docker/docker/pkg/symlink", + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { - "ImportPath": "github.com/docker/docker/pkg/tarsum", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "ImportPath": "github.com/docker/docker/pkg/system", + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/pkg/term", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/pkg/units", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { "ImportPath": "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar", - "Comment": "v1.4.1-1720-gc1639a7", - "Rev": "c1639a7e4e4667e25dd8c39eeccb30b8c8fc6357" + "Comment": "v1.4.1-1223-g7d2188f", + "Rev": "7d2188f9955d3f2002ff8c2e566ef84121de5217" }, { - "ImportPath": "github.com/docker/libtrust", - "Rev": "c54fbb67c1f1e68d7d6f8d2ad7c9360404616a41" + "ImportPath": "github.com/docker/libcontainer", + "Comment": "v1.4.0-52-gd7dea0e", + "Rev": "d7dea0e925315bab640115053204c16718839b1e" }, { "ImportPath": "github.com/docker/spdystream", - "Rev": "29e1da2890f60336f98d0b3bf28b05070aa2ee4d" + "Rev": "e731c8f9f19ffd7e51a469a2de1580c1dfbb4fae" }, { "ImportPath": "github.com/elazarl/go-bindata-assetfs", @@ -558,22 +593,31 @@ }, { "ImportPath": "github.com/emicklei/go-restful", - "Comment": "v1.1.2-50-g692a500", - "Rev": "692a50017a7049b26cf7ea4ccfc0d8c77369a793" + "Comment": "v1.1.3-10-g62dc65d", + "Rev": "62dc65d6e51525418cad2bb6f292d3cf7c5e9d0a" + }, + { + "ImportPath": "github.com/evanphx/json-patch", + "Rev": "7dd4489c2eb6073e5a9d7746c3274c5b5f0387df" }, { "ImportPath": "github.com/fsouza/go-dockerclient", - "Comment": "0.2.1-357-gd197177", - "Rev": "e1e2cc5b83662b894c6871db875c37eb3725a045" + "Comment": "0.2.1-426-gbd32742", + "Rev": "bd32742a065748632a860c9ffd8adf42cc4e140f" }, { "ImportPath": "github.com/getsentry/raven-go", - "Rev": "3fd636ed242c26c0f55bc9ee1fe47e1d6d2d77f7" + "Rev": "86cd4063c535cbbcbf43d84424dbd5911ab1b818" }, { "ImportPath": "github.com/ghodss/yaml", "Rev": "588cb435e59ee8b6c2795482887755841ad67207" }, + { + "ImportPath": "github.com/godbus/dbus", + "Comment": "0-7-g939230d", + "Rev": "939230d2086a4f1870e04c52e0a376c25bae0ec4" + }, { "ImportPath": "github.com/golang/glog", "Rev": "44145f04b68cf362d9c4df2182967c2275eaefed" @@ -586,23 +630,98 @@ "ImportPath": "github.com/golang/protobuf/proto", "Rev": "7f07925444bb51fa4cf9dfe6f7661876f8852275" }, + { + "ImportPath": "github.com/google/cadvisor/api", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, { "ImportPath": "github.com/google/cadvisor/client", - "Comment": "0.6.2", - "Rev": "89088df70eca64cf9d6b9a23a3d2bc21a30916d6" + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/container", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/events", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" }, { - "ImportPath": "github.com/google/cadvisor/info", - "Comment": "0.6.2", - "Rev": "89088df70eca64cf9d6b9a23a3d2bc21a30916d6" + "ImportPath": "github.com/google/cadvisor/fs", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/healthz", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/http", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/info/v1", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/info/v2", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/manager", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/metrics", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/pages", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/storage", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/summary", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/utils", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/validate", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" + }, + { + "ImportPath": "github.com/google/cadvisor/version", + "Comment": "0.10.1-62-ge78e515", + "Rev": "e78e515723d9eb387e5fd865a811f6263e946a06" }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "aef70dacbc78771e35beb261bb3a72986adf7906" + "Rev": "bbcb9da2d746f8bdbd6a936686a0a6067ada0ec5" }, { "ImportPath": "github.com/gorilla/context", - "Rev": "a8662172a384296ff59d28f837e151643c662985" + "Rev": "215affda49addc4c8ef7e2534915df2c8c35c6cd" }, { "ImportPath": "github.com/gorilla/handlers", @@ -645,101 +764,91 @@ }, { "ImportPath": "github.com/openshift/source-to-image/pkg/api", - "Comment": "v0.2", + "Comment": "v0.2-8-gc0c154e", "Rev": "c0c154efcba27ea5693c428bfe28560c220b4850" }, { "ImportPath": "github.com/openshift/source-to-image/pkg/build", - "Comment": "v0.2", + "Comment": "v0.2-8-gc0c154e", "Rev": "c0c154efcba27ea5693c428bfe28560c220b4850" }, { "ImportPath": "github.com/openshift/source-to-image/pkg/docker", - "Comment": "v0.2", + "Comment": "v0.2-8-gc0c154e", "Rev": "c0c154efcba27ea5693c428bfe28560c220b4850" }, { "ImportPath": "github.com/openshift/source-to-image/pkg/errors", - "Comment": "v0.2", + "Comment": "v0.2-8-gc0c154e", "Rev": "c0c154efcba27ea5693c428bfe28560c220b4850" }, { "ImportPath": "github.com/openshift/source-to-image/pkg/git", - "Comment": "v0.2", + "Comment": "v0.2-8-gc0c154e", "Rev": "c0c154efcba27ea5693c428bfe28560c220b4850" }, { "ImportPath": "github.com/openshift/source-to-image/pkg/scripts", - "Comment": "v0.2", + "Comment": "v0.2-8-gc0c154e", "Rev": "c0c154efcba27ea5693c428bfe28560c220b4850" }, { "ImportPath": "github.com/openshift/source-to-image/pkg/tar", - "Comment": "v0.2", + "Comment": "v0.2-8-gc0c154e", "Rev": "c0c154efcba27ea5693c428bfe28560c220b4850" }, { "ImportPath": "github.com/openshift/source-to-image/pkg/util", - "Comment": "v0.2", + "Comment": "v0.2-8-gc0c154e", "Rev": "c0c154efcba27ea5693c428bfe28560c220b4850" }, { "ImportPath": "github.com/pkg/profile", "Rev": "c795610ec6e479e5795f7852db65ea15073674a6" }, - { - "ImportPath": "github.com/prometheus/client_golang/_vendor/goautoneg", - "Comment": "0.1.0-11-gc70db11", - "Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06" - }, - { - "ImportPath": "github.com/prometheus/client_golang/_vendor/perks/quantile", - "Comment": "0.1.0-11-gc70db11", - "Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06" - }, { "ImportPath": "github.com/prometheus/client_golang/model", - "Comment": "0.1.0-11-gc70db11", - "Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06" + "Comment": "0.2.0-5-gde5f7a2", + "Rev": "de5f7a2db9d883392ce3ad667087280fe1ff9cea" }, { "ImportPath": "github.com/prometheus/client_golang/prometheus", - "Comment": "0.1.0-11-gc70db11", - "Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06" + "Comment": "0.2.0-5-gde5f7a2", + "Rev": "de5f7a2db9d883392ce3ad667087280fe1ff9cea" }, { "ImportPath": "github.com/prometheus/client_golang/text", - "Comment": "0.1.0-11-gc70db11", - "Rev": "c70db11f1ee77a34066aa41345dca4b105c2ed06" + "Comment": "0.2.0-5-gde5f7a2", + "Rev": "de5f7a2db9d883392ce3ad667087280fe1ff9cea" }, { "ImportPath": "github.com/prometheus/client_model/go", - "Comment": "model-0.0.2-10-gbc9454c", - "Rev": "bc9454ca562dc050e060ea61a1c0e562a189850f" + "Comment": "model-0.0.2-12-gfa8ad6f", + "Rev": "fa8ad6fec33561be4280a8f0514318c79d7f6cb6" }, { "ImportPath": "github.com/prometheus/procfs", - "Rev": "92faa308558161acab0ada1db048e9996ecec160" + "Rev": "6c34ef819e19b4e16f410100ace4aa006f0e3bf8" }, { "ImportPath": "github.com/skynetservices/skydns/backends/etcd", - "Comment": "2.0.1d-50-g73f6fae", - "Rev": "73f6fae00933f23a08b1e436537721e499d4410a" + "Comment": "2.0.1d-54-g5c2bf88", + "Rev": "5c2bf889497b9857586b4fd6703fdc1b7d5638bd" }, { "ImportPath": "github.com/skynetservices/skydns/cache", - "Comment": "2.0.1d-50-g73f6fae", - "Rev": "73f6fae00933f23a08b1e436537721e499d4410a" + "Comment": "2.0.1d-54-g5c2bf88", + "Rev": "5c2bf889497b9857586b4fd6703fdc1b7d5638bd" }, { "ImportPath": "github.com/skynetservices/skydns/msg", - "Comment": "2.0.1d-50-g73f6fae", - "Rev": "73f6fae00933f23a08b1e436537721e499d4410a" + "Comment": "2.0.1d-54-g5c2bf88", + "Rev": "5c2bf889497b9857586b4fd6703fdc1b7d5638bd" }, { "ImportPath": "github.com/skynetservices/skydns/server", - "Comment": "2.0.1d-50-g73f6fae", - "Rev": "73f6fae00933f23a08b1e436537721e499d4410a" + "Comment": "2.0.1d-54-g5c2bf88", + "Rev": "5c2bf889497b9857586b4fd6703fdc1b7d5638bd" }, { "ImportPath": "github.com/spf13/cobra", @@ -749,6 +858,18 @@ "ImportPath": "github.com/spf13/pflag", "Rev": "370c3171201099fa6b466db45c8a032cbce33d8d" }, + { + "ImportPath": "github.com/stretchr/objx", + "Rev": "d40df0cc104c06eae2dfe03d7dddb83802d52f9a" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "e4ec8152c15fc46bd5056ce65997a07c7d415325" + }, + { + "ImportPath": "github.com/stretchr/testify/mock", + "Rev": "e4ec8152c15fc46bd5056ce65997a07c7d415325" + }, { "ImportPath": "golang.org/x/net/context", "Rev": "cbcac7bb8415db9b6cb4d1ebab1dc9afbd688b97" diff --git a/Godeps/_workspace/src/github.com/prometheus/client_golang/_vendor/goautoneg/Makefile b/Godeps/_workspace/src/bitbucket.org/ww/goautoneg/Makefile similarity index 100% rename from Godeps/_workspace/src/github.com/prometheus/client_golang/_vendor/goautoneg/Makefile rename to Godeps/_workspace/src/bitbucket.org/ww/goautoneg/Makefile diff --git a/Godeps/_workspace/src/github.com/prometheus/client_golang/_vendor/goautoneg/README.txt b/Godeps/_workspace/src/bitbucket.org/ww/goautoneg/README.txt similarity index 100% rename from Godeps/_workspace/src/github.com/prometheus/client_golang/_vendor/goautoneg/README.txt rename to Godeps/_workspace/src/bitbucket.org/ww/goautoneg/README.txt diff --git a/Godeps/_workspace/src/github.com/prometheus/client_golang/_vendor/goautoneg/autoneg.go b/Godeps/_workspace/src/bitbucket.org/ww/goautoneg/autoneg.go similarity index 100% rename from Godeps/_workspace/src/github.com/prometheus/client_golang/_vendor/goautoneg/autoneg.go rename to Godeps/_workspace/src/bitbucket.org/ww/goautoneg/autoneg.go diff --git a/Godeps/_workspace/src/github.com/prometheus/client_golang/_vendor/goautoneg/autoneg_test.go b/Godeps/_workspace/src/bitbucket.org/ww/goautoneg/autoneg_test.go similarity index 100% rename from Godeps/_workspace/src/github.com/prometheus/client_golang/_vendor/goautoneg/autoneg_test.go rename to Godeps/_workspace/src/bitbucket.org/ww/goautoneg/autoneg_test.go diff --git a/Godeps/_workspace/src/code.google.com/p/go.exp/inotify/inotify_linux.go b/Godeps/_workspace/src/code.google.com/p/go.exp/inotify/inotify_linux.go new file mode 100644 index 000000000000..f671f47a1308 --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.exp/inotify/inotify_linux.go @@ -0,0 +1,300 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package inotify implements a wrapper for the Linux inotify system. + +Example: + watcher, err := inotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + err = watcher.Watch("/tmp") + if err != nil { + log.Fatal(err) + } + for { + select { + case ev := <-watcher.Event: + log.Println("event:", ev) + case err := <-watcher.Error: + log.Println("error:", err) + } + } + +*/ +package inotify + +import ( + "errors" + "fmt" + "os" + "strings" + "sync" + "syscall" + "unsafe" +) + +type Event struct { + Mask uint32 // Mask of events + Cookie uint32 // Unique cookie associating related events (for rename(2)) + Name string // File name (optional) +} + +type watch struct { + wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) + flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) +} + +type Watcher struct { + mu sync.Mutex + fd int // File descriptor (as returned by the inotify_init() syscall) + watches map[string]*watch // Map of inotify watches (key: path) + paths map[int]string // Map of watched paths (key: watch descriptor) + Error chan error // Errors are sent on this channel + Event chan *Event // Events are returned on this channel + done chan bool // Channel for sending a "quit message" to the reader goroutine + isClosed bool // Set to true when Close() is first called +} + +// NewWatcher creates and returns a new inotify instance using inotify_init(2) +func NewWatcher() (*Watcher, error) { + fd, errno := syscall.InotifyInit() + if fd == -1 { + return nil, os.NewSyscallError("inotify_init", errno) + } + w := &Watcher{ + fd: fd, + watches: make(map[string]*watch), + paths: make(map[int]string), + Event: make(chan *Event), + Error: make(chan error), + done: make(chan bool, 1), + } + + go w.readEvents() + return w, nil +} + +// Close closes an inotify watcher instance +// It sends a message to the reader goroutine to quit and removes all watches +// associated with the inotify instance +func (w *Watcher) Close() error { + if w.isClosed { + return nil + } + w.isClosed = true + + // Send "quit" message to the reader goroutine + w.done <- true + for path := range w.watches { + w.RemoveWatch(path) + } + + return nil +} + +// AddWatch adds path to the watched file set. +// The flags are interpreted as described in inotify_add_watch(2). +func (w *Watcher) AddWatch(path string, flags uint32) error { + if w.isClosed { + return errors.New("inotify instance already closed") + } + + watchEntry, found := w.watches[path] + if found { + watchEntry.flags |= flags + flags |= syscall.IN_MASK_ADD + } + + w.mu.Lock() // synchronize with readEvents goroutine + + wd, err := syscall.InotifyAddWatch(w.fd, path, flags) + if err != nil { + w.mu.Unlock() + return &os.PathError{ + Op: "inotify_add_watch", + Path: path, + Err: err, + } + } + + if !found { + w.watches[path] = &watch{wd: uint32(wd), flags: flags} + w.paths[wd] = path + } + w.mu.Unlock() + return nil +} + +// Watch adds path to the watched file set, watching all events. +func (w *Watcher) Watch(path string) error { + return w.AddWatch(path, IN_ALL_EVENTS) +} + +// RemoveWatch removes path from the watched file set. +func (w *Watcher) RemoveWatch(path string) error { + watch, ok := w.watches[path] + if !ok { + return errors.New(fmt.Sprintf("can't remove non-existent inotify watch for: %s", path)) + } + success, errno := syscall.InotifyRmWatch(w.fd, watch.wd) + if success == -1 { + return os.NewSyscallError("inotify_rm_watch", errno) + } + delete(w.watches, path) + return nil +} + +// readEvents reads from the inotify file descriptor, converts the +// received events into Event objects and sends them via the Event channel +func (w *Watcher) readEvents() { + var buf [syscall.SizeofInotifyEvent * 4096]byte + + for { + n, err := syscall.Read(w.fd, buf[:]) + // See if there is a message on the "done" channel + var done bool + select { + case done = <-w.done: + default: + } + + // If EOF or a "done" message is received + if n == 0 || done { + // The syscall.Close can be slow. Close + // w.Event first. + close(w.Event) + err := syscall.Close(w.fd) + if err != nil { + w.Error <- os.NewSyscallError("close", err) + } + close(w.Error) + return + } + if n < 0 { + w.Error <- os.NewSyscallError("read", err) + continue + } + if n < syscall.SizeofInotifyEvent { + w.Error <- errors.New("inotify: short read in readEvents()") + continue + } + + var offset uint32 = 0 + // We don't know how many events we just read into the buffer + // While the offset points to at least one whole event... + for offset <= uint32(n-syscall.SizeofInotifyEvent) { + // Point "raw" to the event in the buffer + raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset])) + event := new(Event) + event.Mask = uint32(raw.Mask) + event.Cookie = uint32(raw.Cookie) + nameLen := uint32(raw.Len) + // If the event happened to the watched directory or the watched file, the kernel + // doesn't append the filename to the event, but we would like to always fill the + // the "Name" field with a valid filename. We retrieve the path of the watch from + // the "paths" map. + w.mu.Lock() + event.Name = w.paths[int(raw.Wd)] + w.mu.Unlock() + if nameLen > 0 { + // Point "bytes" at the first byte of the filename + bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[offset+syscall.SizeofInotifyEvent])) + // The filename is padded with NUL bytes. TrimRight() gets rid of those. + event.Name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") + } + // Send the event on the events channel + w.Event <- event + + // Move to the next event in the buffer + offset += syscall.SizeofInotifyEvent + nameLen + } + } +} + +// String formats the event e in the form +// "filename: 0xEventMask = IN_ACCESS|IN_ATTRIB_|..." +func (e *Event) String() string { + var events string = "" + + m := e.Mask + for _, b := range eventBits { + if m&b.Value != 0 { + m &^= b.Value + events += "|" + b.Name + } + } + + if m != 0 { + events += fmt.Sprintf("|%#x", m) + } + if len(events) > 0 { + events = " == " + events[1:] + } + + return fmt.Sprintf("%q: %#x%s", e.Name, e.Mask, events) +} + +const ( + // Options for inotify_init() are not exported + // IN_CLOEXEC uint32 = syscall.IN_CLOEXEC + // IN_NONBLOCK uint32 = syscall.IN_NONBLOCK + + // Options for AddWatch + IN_DONT_FOLLOW uint32 = syscall.IN_DONT_FOLLOW + IN_ONESHOT uint32 = syscall.IN_ONESHOT + IN_ONLYDIR uint32 = syscall.IN_ONLYDIR + + // The "IN_MASK_ADD" option is not exported, as AddWatch + // adds it automatically, if there is already a watch for the given path + // IN_MASK_ADD uint32 = syscall.IN_MASK_ADD + + // Events + IN_ACCESS uint32 = syscall.IN_ACCESS + IN_ALL_EVENTS uint32 = syscall.IN_ALL_EVENTS + IN_ATTRIB uint32 = syscall.IN_ATTRIB + IN_CLOSE uint32 = syscall.IN_CLOSE + IN_CLOSE_NOWRITE uint32 = syscall.IN_CLOSE_NOWRITE + IN_CLOSE_WRITE uint32 = syscall.IN_CLOSE_WRITE + IN_CREATE uint32 = syscall.IN_CREATE + IN_DELETE uint32 = syscall.IN_DELETE + IN_DELETE_SELF uint32 = syscall.IN_DELETE_SELF + IN_MODIFY uint32 = syscall.IN_MODIFY + IN_MOVE uint32 = syscall.IN_MOVE + IN_MOVED_FROM uint32 = syscall.IN_MOVED_FROM + IN_MOVED_TO uint32 = syscall.IN_MOVED_TO + IN_MOVE_SELF uint32 = syscall.IN_MOVE_SELF + IN_OPEN uint32 = syscall.IN_OPEN + + // Special events + IN_ISDIR uint32 = syscall.IN_ISDIR + IN_IGNORED uint32 = syscall.IN_IGNORED + IN_Q_OVERFLOW uint32 = syscall.IN_Q_OVERFLOW + IN_UNMOUNT uint32 = syscall.IN_UNMOUNT +) + +var eventBits = []struct { + Value uint32 + Name string +}{ + {IN_ACCESS, "IN_ACCESS"}, + {IN_ATTRIB, "IN_ATTRIB"}, + {IN_CLOSE, "IN_CLOSE"}, + {IN_CLOSE_NOWRITE, "IN_CLOSE_NOWRITE"}, + {IN_CLOSE_WRITE, "IN_CLOSE_WRITE"}, + {IN_CREATE, "IN_CREATE"}, + {IN_DELETE, "IN_DELETE"}, + {IN_DELETE_SELF, "IN_DELETE_SELF"}, + {IN_MODIFY, "IN_MODIFY"}, + {IN_MOVE, "IN_MOVE"}, + {IN_MOVED_FROM, "IN_MOVED_FROM"}, + {IN_MOVED_TO, "IN_MOVED_TO"}, + {IN_MOVE_SELF, "IN_MOVE_SELF"}, + {IN_OPEN, "IN_OPEN"}, + {IN_ISDIR, "IN_ISDIR"}, + {IN_IGNORED, "IN_IGNORED"}, + {IN_Q_OVERFLOW, "IN_Q_OVERFLOW"}, + {IN_UNMOUNT, "IN_UNMOUNT"}, +} diff --git a/Godeps/_workspace/src/code.google.com/p/go.exp/inotify/inotify_linux_test.go b/Godeps/_workspace/src/code.google.com/p/go.exp/inotify/inotify_linux_test.go new file mode 100644 index 000000000000..1685b772ec1e --- /dev/null +++ b/Godeps/_workspace/src/code.google.com/p/go.exp/inotify/inotify_linux_test.go @@ -0,0 +1,107 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux + +package inotify + +import ( + "io/ioutil" + "os" + "sync/atomic" + "testing" + "time" +) + +func TestInotifyEvents(t *testing.T) { + // Create an inotify watcher instance and initialize it + watcher, err := NewWatcher() + if err != nil { + t.Fatalf("NewWatcher failed: %s", err) + } + + dir, err := ioutil.TempDir("", "inotify") + if err != nil { + t.Fatalf("TempDir failed: %s", err) + } + defer os.RemoveAll(dir) + + // Add a watch for "_test" + err = watcher.Watch(dir) + if err != nil { + t.Fatalf("Watch failed: %s", err) + } + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Error { + t.Fatalf("error received: %s", err) + } + }() + + testFile := dir + "/TestInotifyEvents.testfile" + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Event + var eventsReceived int32 = 0 + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == testFile { + atomic.AddInt32(&eventsReceived, 1) + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the inotify event queue + _, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 1 s to be sure + time.Sleep(1 * time.Second) + if atomic.AddInt32(&eventsReceived, 0) == 0 { + t.Fatal("inotify event hasn't been received after 1 second") + } + + // Try closing the inotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(1 * time.Second): + t.Fatal("event stream was not closed after 1 second") + } +} + +func TestInotifyClose(t *testing.T) { + watcher, _ := NewWatcher() + watcher.Close() + + done := make(chan bool) + go func() { + watcher.Close() + done <- true + }() + + select { + case <-done: + case <-time.After(50 * time.Millisecond): + t.Fatal("double Close() test failed: second Close() call didn't return") + } + + err := watcher.Watch(os.TempDir()) + if err == nil { + t.Fatal("expected error on Watch() after Close(), got nil") + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/conversion.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/conversion.go index 372adb760994..49e3ff86f75c 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/conversion.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/conversion.go @@ -39,17 +39,22 @@ func init() { *out = *in.Copy() return nil }, - // Convert ContainerManifest to BoundPod - func(in *ContainerManifest, out *BoundPod, s conversion.Scope) error { + // Convert ContainerManifest to Pod + func(in *ContainerManifest, out *Pod, s conversion.Scope) error { out.Spec.Containers = in.Containers out.Spec.Volumes = in.Volumes out.Spec.RestartPolicy = in.RestartPolicy out.Spec.DNSPolicy = in.DNSPolicy out.Name = in.ID out.UID = in.UUID + + if in.ID != "" { + out.SelfLink = "/api/v1beta1/pods/" + in.ID + } + return nil }, - func(in *BoundPod, out *ContainerManifest, s conversion.Scope) error { + func(in *Pod, out *ContainerManifest, s conversion.Scope) error { out.Containers = in.Spec.Containers out.Volumes = in.Spec.Volumes out.RestartPolicy = in.Spec.RestartPolicy @@ -61,7 +66,7 @@ func init() { }, // ContainerManifestList - func(in *ContainerManifestList, out *BoundPods, s conversion.Scope) error { + func(in *ContainerManifestList, out *PodList, s conversion.Scope) error { if err := s.Convert(&in.Items, &out.Items, 0); err != nil { return err } @@ -71,7 +76,7 @@ func init() { } return nil }, - func(in *BoundPods, out *ContainerManifestList, s conversion.Scope) error { + func(in *PodList, out *ContainerManifestList, s conversion.Scope) error { if err := s.Convert(&in.Items, &out.Items, 0); err != nil { return err } @@ -79,20 +84,6 @@ func init() { return nil }, - // Convert Pod to BoundPod - func(in *Pod, out *BoundPod, s conversion.Scope) error { - if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { - return err - } - // Only copy a subset of fields, and override manifest attributes with the pod - // metadata - out.UID = in.UID - out.Name = in.Name - out.Namespace = in.Namespace - out.CreationTimestamp = in.CreationTimestamp - return nil - }, - // Conversion between Manifest and PodSpec func(in *PodSpec, out *ContainerManifest, s conversion.Scope) error { if err := s.Convert(&in.Volumes, &out.Volumes, 0); err != nil { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/errors.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/errors.go index 403beecf5029..9775c4cfcf47 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/errors.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/errors.go @@ -262,6 +262,18 @@ func IsServerTimeout(err error) bool { return reasonForError(err) == api.StatusReasonServerTimeout } +// IsStatusError determines if err is an API Status error received from the master. +func IsStatusError(err error) bool { + _, ok := err.(*StatusError) + return ok +} + +// IsUnexpectedObjectError determines if err is due to an unexpected object from the master. +func IsUnexpectedObjectError(err error) bool { + _, ok := err.(*UnexpectedObjectError) + return ok +} + func reasonForError(err error) api.StatusReason { switch t := err.(type) { case *StatusError: diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/errors_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/errors_test.go index ae57f38818ca..34bcccf6ad83 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/errors_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/errors_test.go @@ -126,7 +126,7 @@ func TestNewInvalid(t *testing.T) { }, }, { - NewFieldRequired("field[0].name", "bar"), + NewFieldRequired("field[0].name"), &api.StatusDetails{ Kind: "kind", ID: "name", diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/validation.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/validation.go index f3431a495df8..525e4d6e336e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/validation.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/validation.go @@ -52,6 +52,8 @@ const ( // values which would be accepted by some api instances, but which would invoke behavior // not permitted by this api instance (such as due to stricter security policy). ValidationErrorTypeForbidden ValidationErrorType = "FieldValueForbidden" + // ValidationErrorTypeTooLong is used to report that given value is too long. + ValidationErrorTypeTooLong ValidationErrorType = "FieldValueTooLong" ) // String converts a ValidationErrorType into its corresponding error message. @@ -69,6 +71,8 @@ func (t ValidationErrorType) String() string { return "unsupported value" case ValidationErrorTypeForbidden: return "forbidden" + case ValidationErrorTypeTooLong: + return "too long" default: glog.Errorf("unrecognized validation type: %#v", t) return "" @@ -100,9 +104,8 @@ func (v *ValidationError) Error() string { } // NewFieldRequired returns a *ValidationError indicating "value required" -// TODO: remove "value" -func NewFieldRequired(field string, value interface{}) *ValidationError { - return &ValidationError{ValidationErrorTypeRequired, field, value, ""} +func NewFieldRequired(field string) *ValidationError { + return &ValidationError{ValidationErrorTypeRequired, field, "", ""} } // NewFieldInvalid returns a *ValidationError indicating "invalid value" @@ -130,6 +133,10 @@ func NewFieldNotFound(field string, value interface{}) *ValidationError { return &ValidationError{ValidationErrorTypeNotFound, field, value, ""} } +func NewFieldTooLong(field string, value interface{}) *ValidationError { + return &ValidationError{ValidationErrorTypeTooLong, field, value, ""} +} + type ValidationErrorList []error // Prefix adds a prefix to the Field of every ValidationError in the list. diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/validation_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/validation_test.go index a312a2849547..b223229f4f85 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/validation_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/validation_test.go @@ -43,7 +43,7 @@ func TestMakeFuncs(t *testing.T) { ValidationErrorTypeNotFound, }, { - func() *ValidationError { return NewFieldRequired("f", "v") }, + func() *ValidationError { return NewFieldRequired("f") }, ValidationErrorTypeRequired, }, } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/helpers.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/helpers.go index e05538e86a38..621ce4ed80da 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/helpers.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/helpers.go @@ -60,6 +60,9 @@ var Semantic = conversion.EqualitiesOrDie( } return a.Amount.Cmp(b.Amount) == 0 }, + func(a, b util.Time) bool { + return a.UTC() == b.UTC() + }, ) var standardResources = util.NewStringSet( @@ -73,3 +76,11 @@ var standardResources = util.NewStringSet( func IsStandardResourceName(str string) bool { return standardResources.Has(str) } + +// NewDeleteOptions returns a DeleteOptions indicating the resource should +// be deleted within the specified grace period. Use zero to indicate +// immediate deletion. If you would prefer to use the default grace period, +// use &api.DeleteOptions{} directly. +func NewDeleteOptions(grace int64) *DeleteOptions { + return &DeleteOptions{GracePeriodSeconds: &grace} +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest/latest.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest/latest.go index 012ee85b7657..f46d153a4d99 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest/latest.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest/latest.go @@ -50,11 +50,6 @@ var Codec = v1beta1.Codec // accessor is the shared static metadata accessor for the API. var accessor = meta.NewAccessor() -// ResourceVersioner describes a default versioner that can handle all types -// of versioning. -// TODO: when versioning changes, make this part of each API definition. -var ResourceVersioner = runtime.ResourceVersioner(accessor) - // SelfLinker can set or get the SelfLink field of all API types. // TODO: when versioning changes, make this part of each API definition. // TODO(lavalamp): Combine SelfLinker & ResourceVersioner interfaces, force all uses diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest/latest_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest/latest_test.go index f0582b5a18dd..4faa05f05ea5 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest/latest_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest/latest_test.go @@ -18,60 +18,16 @@ package latest import ( "encoding/json" - "math/rand" "testing" internal "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - apitesting "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testing" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) -func TestInternalRoundTrip(t *testing.T) { - latest := "v1beta2" - - seed := rand.Int63() - apiObjectFuzzer := apitesting.FuzzerFor(t, "", rand.NewSource(seed)) - for k := range internal.Scheme.KnownTypes("") { - obj, err := internal.Scheme.New("", k) - if err != nil { - t.Errorf("%s: unexpected error: %v", k, err) - continue - } - apiObjectFuzzer.Fuzz(obj) - - newer, err := internal.Scheme.New(latest, k) - if err != nil { - t.Errorf("%s: unexpected error: %v", k, err) - continue - } - - if err := internal.Scheme.Convert(obj, newer); err != nil { - t.Errorf("unable to convert %#v to %#v: %v", obj, newer, err) - continue - } - - actual, err := internal.Scheme.New("", k) - if err != nil { - t.Errorf("%s: unexpected error: %v", k, err) - continue - } - - if err := internal.Scheme.Convert(newer, actual); err != nil { - t.Errorf("unable to convert %#v to %#v: %v", newer, actual, err) - continue - } - - if !internal.Semantic.DeepEqual(obj, actual) { - t.Errorf("%s: diff %s", k, util.ObjectDiff(obj, actual)) - } - } -} - func TestResourceVersioner(t *testing.T) { pod := internal.Pod{ObjectMeta: internal.ObjectMeta{ResourceVersion: "10"}} - version, err := ResourceVersioner.ResourceVersion(&pod) + version, err := accessor.ResourceVersion(&pod) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -80,7 +36,7 @@ func TestResourceVersioner(t *testing.T) { } podList := internal.PodList{ListMeta: internal.ListMeta{ResourceVersion: "10"}} - version, err = ResourceVersioner.ResourceVersion(&podList) + version, err = accessor.ResourceVersion(&podList) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta.go index 5782be6558fe..2a0b518fd1cc 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta.go @@ -35,16 +35,27 @@ func HasObjectMetaSystemFieldValues(meta *ObjectMeta) bool { len(meta.UID) != 0 } -// GetObjectMetaPtr returns a pointer to a provided object's ObjectMeta. +// ObjectMetaFor returns a pointer to a provided object's ObjectMeta. // TODO: allow runtime.Unknown to extract this object func ObjectMetaFor(obj runtime.Object) (*ObjectMeta, error) { v, err := conversion.EnforcePtr(obj) if err != nil { return nil, err } - var objectMeta *ObjectMeta - if err := runtime.FieldPtr(v, "ObjectMeta", &objectMeta); err != nil { + var meta *ObjectMeta + err = runtime.FieldPtr(v, "ObjectMeta", &meta) + return meta, err +} + +// ListMetaFor returns a pointer to a provided object's ListMeta, +// or an error if the object does not have that pointer. +// TODO: allow runtime.Unknown to extract this object +func ListMetaFor(obj runtime.Object) (*ListMeta, error) { + v, err := conversion.EnforcePtr(obj) + if err != nil { return nil, err } - return objectMeta, nil + var meta *ListMeta + err = runtime.FieldPtr(v, "ListMeta", &meta) + return meta, err } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/register.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/register.go index 976b409c37a1..81d01572076e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/register.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/register.go @@ -34,6 +34,7 @@ func init() { &Service{}, &NodeList{}, &Node{}, + &NodeInfo{}, &Status{}, &Endpoints{}, &EndpointsList{}, @@ -42,18 +43,16 @@ func init() { &EventList{}, &ContainerManifest{}, &ContainerManifestList{}, - &BoundPod{}, - &BoundPods{}, &List{}, &LimitRange{}, &LimitRangeList{}, &ResourceQuota{}, &ResourceQuotaList{}, - &ResourceQuotaUsage{}, &Namespace{}, &NamespaceList{}, &Secret{}, &SecretList{}, + &DeleteOptions{}, ) // Legacy names are supported Scheme.AddKnownTypeWithName("", "Minion", &Node{}) @@ -70,6 +69,7 @@ func (*ServiceList) IsAnAPIObject() {} func (*Endpoints) IsAnAPIObject() {} func (*EndpointsList) IsAnAPIObject() {} func (*Node) IsAnAPIObject() {} +func (*NodeInfo) IsAnAPIObject() {} func (*NodeList) IsAnAPIObject() {} func (*Binding) IsAnAPIObject() {} func (*Status) IsAnAPIObject() {} @@ -77,15 +77,13 @@ func (*Event) IsAnAPIObject() {} func (*EventList) IsAnAPIObject() {} func (*ContainerManifest) IsAnAPIObject() {} func (*ContainerManifestList) IsAnAPIObject() {} -func (*BoundPod) IsAnAPIObject() {} -func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} -func (*ResourceQuotaUsage) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/delete.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/delete.go new file mode 100644 index 000000000000..a636e65e2986 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/delete.go @@ -0,0 +1,51 @@ +/* +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 rest + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// RESTDeleteStrategy defines deletion behavior on an object that follows Kubernetes +// API conventions. +type RESTDeleteStrategy interface { + runtime.ObjectTyper + + // CheckGracefulDelete should return true if the object can be gracefully deleted and set + // any default values on the DeleteOptions. + CheckGracefulDelete(obj runtime.Object, options *api.DeleteOptions) bool +} + +// BeforeDelete tests whether the object can be gracefully deleted. If graceful is set the object +// should be gracefully deleted, if gracefulPending is set the object has already been gracefully deleted +// (and the provided grace period is longer than the time to deletion), and an error is returned if the +// condition cannot be checked or the gracePeriodSeconds is invalid. The options argument may be updated with +// default values if graceful is true. +func BeforeDelete(strategy RESTDeleteStrategy, ctx api.Context, obj runtime.Object, options *api.DeleteOptions) (graceful, gracefulPending bool, err error) { + if strategy == nil { + return false, false, nil + } + _, _, kerr := objectMetaAndKind(strategy, obj) + if kerr != nil { + return false, false, kerr + } + if !strategy.CheckGracefulDelete(obj, options) { + return false, false, nil + } + return true, false, nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest/resttest.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest/resttest.go index 1b83ef5cd8ac..0690bfc07955 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest/resttest.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest/resttest.go @@ -196,3 +196,63 @@ func (t *Tester) TestCreateRejectsNamespace(valid runtime.Object) { t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error()) } } + +func (t *Tester) TestDeleteGraceful(createFn func() runtime.Object, expectedGrace int64, wasGracefulFn func() bool) { + t.TestDeleteGracefulHasDefault(createFn(), expectedGrace, wasGracefulFn) + t.TestDeleteGracefulUsesZeroOnNil(createFn(), 0) +} + +func (t *Tester) TestDeleteNoGraceful(createFn func() runtime.Object, wasGracefulFn func() bool) { + existing := createFn() + objectMeta, err := api.ObjectMetaFor(existing) + if err != nil { + t.Fatalf("object does not have ObjectMeta: %v\n%#v", err, existing) + } + + ctx := api.WithNamespace(api.NewContext(), objectMeta.Namespace) + _, err = t.storage.(apiserver.RESTGracefulDeleter).Delete(ctx, objectMeta.Name, api.NewDeleteOptions(10)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if _, err := t.storage.(apiserver.RESTGetter).Get(ctx, objectMeta.Name); !errors.IsNotFound(err) { + t.Errorf("unexpected error, object should not exist: %v", err) + } + if wasGracefulFn() { + t.Errorf("resource should not support graceful delete") + } +} + +func (t *Tester) TestDeleteGracefulHasDefault(existing runtime.Object, expectedGrace int64, wasGracefulFn func() bool) { + objectMeta, err := api.ObjectMetaFor(existing) + if err != nil { + t.Fatalf("object does not have ObjectMeta: %v\n%#v", err, existing) + } + + ctx := api.WithNamespace(api.NewContext(), objectMeta.Namespace) + _, err = t.storage.(apiserver.RESTGracefulDeleter).Delete(ctx, objectMeta.Name, &api.DeleteOptions{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if _, err := t.storage.(apiserver.RESTGetter).Get(ctx, objectMeta.Name); err != nil { + t.Errorf("unexpected error, object should exist: %v", err) + } + if !wasGracefulFn() { + t.Errorf("did not gracefully delete resource") + } +} + +func (t *Tester) TestDeleteGracefulUsesZeroOnNil(existing runtime.Object, expectedGrace int64) { + objectMeta, err := api.ObjectMetaFor(existing) + if err != nil { + t.Fatalf("object does not have ObjectMeta: %v\n%#v", err, existing) + } + + ctx := api.WithNamespace(api.NewContext(), objectMeta.Namespace) + _, err = t.storage.(apiserver.RESTGracefulDeleter).Delete(ctx, objectMeta.Name, nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if _, err := t.storage.(apiserver.RESTGetter).Get(ctx, objectMeta.Name); !errors.IsNotFound(err) { + t.Errorf("unexpected error, object should exist: %v", err) + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/types.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/types.go index f3b6741f82d1..6292bcaae4da 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/types.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/types.go @@ -44,34 +44,6 @@ func AllFuncs(fns ...ObjectFunc) ObjectFunc { } } -// rcStrategy implements behavior for Replication Controllers. -// TODO: move to a replicationcontroller specific package. -type rcStrategy struct { - runtime.ObjectTyper - api.NameGenerator -} - -// ReplicationControllers is the default logic that applies when creating and updating Replication Controller -// objects. -var ReplicationControllers RESTCreateStrategy = rcStrategy{api.Scheme, api.SimpleNameGenerator} - -// NamespaceScoped is true for replication controllers. -func (rcStrategy) NamespaceScoped() bool { - return true -} - -// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. -func (rcStrategy) ResetBeforeCreate(obj runtime.Object) { - controller := obj.(*api.ReplicationController) - controller.Status = api.ReplicationControllerStatus{} -} - -// Validate validates a new replication controller. -func (rcStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { - controller := obj.(*api.ReplicationController) - return validation.ValidateReplicationController(controller) -} - // svcStrategy implements behavior for Services // TODO: move to a service specific package. type svcStrategy struct { @@ -81,7 +53,7 @@ type svcStrategy struct { // Services is the default logic that applies when creating and updating Service // objects. -var Services RESTCreateStrategy = svcStrategy{api.Scheme, api.SimpleNameGenerator} +var Services = svcStrategy{api.Scheme, api.SimpleNameGenerator} // NamespaceScoped is true for services. func (svcStrategy) NamespaceScoped() bool { @@ -100,6 +72,14 @@ func (svcStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { return validation.ValidateService(service) } +func (svcStrategy) AllowCreateOnUpdate() bool { + return true +} + +func (svcStrategy) ValidateUpdate(obj, old runtime.Object) errors.ValidationErrorList { + return validation.ValidateServiceUpdate(old.(*api.Service), obj.(*api.Service)) +} + // nodeStrategy implements behavior for nodes // TODO: move to a node specific package. type nodeStrategy struct { @@ -127,30 +107,3 @@ func (nodeStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { node := obj.(*api.Node) return validation.ValidateMinion(node) } - -// namespaceStrategy implements behavior for nodes -type namespaceStrategy struct { - runtime.ObjectTyper - api.NameGenerator -} - -// Namespaces is the default logic that applies when creating and updating Namespace -// objects. -var Namespaces RESTCreateStrategy = namespaceStrategy{api.Scheme, api.SimpleNameGenerator} - -// NamespaceScoped is false for namespaces. -func (namespaceStrategy) NamespaceScoped() bool { - return false -} - -// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. -func (namespaceStrategy) ResetBeforeCreate(obj runtime.Object) { - _ = obj.(*api.Namespace) - // Namespace allow *all* fields, including status, to be set. -} - -// Validate validates a new namespace. -func (namespaceStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { - namespace := obj.(*api.Namespace) - return validation.ValidateNamespace(namespace) -} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/update_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/update_test.go new file mode 100644 index 000000000000..5817a93a4e3d --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/update_test.go @@ -0,0 +1,111 @@ +/* +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 rest + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func TestBeforeUpdate(t *testing.T) { + tests := []struct { + old runtime.Object + obj runtime.Object + expectErr bool + }{ + { + obj: &api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "#$%%invalid", + }, + }, + old: &api.Service{}, + expectErr: true, + }, + { + obj: &api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "valid", + }, + }, + old: &api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "bar", + Namespace: "valid", + }, + }, + expectErr: true, + }, + { + obj: &api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "valid", + }, + Spec: api.ServiceSpec{ + PortalIP: "1.2.3.4", + }, + }, + old: &api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "valid", + }, + Spec: api.ServiceSpec{ + PortalIP: "4.3.2.1", + }, + }, + expectErr: true, + }, + { + obj: &api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: api.ServiceSpec{ + PortalIP: "1.2.3.4", + Selector: map[string]string{"foo": "bar"}, + }, + }, + old: &api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: api.ServiceSpec{ + PortalIP: "1.2.3.4", + Selector: map[string]string{"bar": "foo"}, + }, + }, + }, + } + for _, test := range tests { + ctx := api.NewDefaultContext() + err := BeforeUpdate(Services, ctx, test.obj, test.old) + if test.expectErr && err == nil { + t.Errorf("unexpected non-error for %v", test) + } + if !test.expectErr && err != nil { + t.Errorf("unexpected error: %v for %v -> %v", err, test.obj, test.old) + } + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/serialization_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/serialization_test.go index 25caa9a59289..af7f5d30ff1d 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/serialization_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/serialization_test.go @@ -32,6 +32,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/davecgh/go-spew/spew" flag "github.com/spf13/pflag" ) @@ -52,20 +53,22 @@ func fuzzInternalObject(t *testing.T, forVersion string, item runtime.Object, se } func roundTrip(t *testing.T, codec runtime.Codec, item runtime.Object) { + printer := spew.ConfigState{DisableMethods: true} + name := reflect.TypeOf(item).Elem().Name() data, err := codec.Encode(item) if err != nil { - t.Errorf("%v: %v (%#v)", name, err, item) + t.Errorf("%v: %v (%s)", name, err, printer.Sprintf("%#v", item)) return } obj2, err := codec.Decode(data) if err != nil { - t.Errorf("0: %v: %v\nCodec: %v\nData: %s\nSource: %#v", name, err, codec, string(data), item) + t.Errorf("0: %v: %v\nCodec: %v\nData: %s\nSource: %#v", name, err, codec, string(data), printer.Sprintf("%#v", item)) return } if !api.Semantic.DeepEqual(item, obj2) { - t.Errorf("1: %v: diff: %v\nCodec: %v\nData: %s\nSource: %#v\nFinal: %#v", name, util.ObjectGoPrintDiff(item, obj2), codec, string(data), item, obj2) + t.Errorf("1: %v: diff: %v\nCodec: %v\nData: %s\nSource: %#v\nFinal: %#v", name, util.ObjectGoPrintDiff(item, obj2), codec, string(data), printer.Sprintf("%#v", item), printer.Sprintf("%#v", obj2)) return } @@ -159,7 +162,7 @@ func TestEncode_Ptr(t *testing.T) { Labels: map[string]string{"name": "foo"}, }, Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/testing/fuzzer.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/testing/fuzzer.go index dbb0e05e2d3c..ea2993655f87 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/testing/fuzzer.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/testing/fuzzer.go @@ -35,7 +35,6 @@ import ( func fuzzOneOf(c fuzz.Continue, objs ...interface{}) { // Use a new fuzzer which cannot populate nil to ensure one obj will be set. - // FIXME: would be nicer to use FuzzOnePtr() and reflect. f := fuzz.New().NilChance(0).NumElements(1, 1) i := c.RandUint64() % uint64(len(objs)) f.Fuzz(objs[i]) @@ -102,19 +101,20 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { j.Spec = api.PodSpec{} c.Fuzz(&j.Spec) }, + func(j *api.Binding, c fuzz.Continue) { + c.Fuzz(&j.ObjectMeta) + j.Target.Name = c.RandString() + }, func(j *api.ReplicationControllerSpec, c fuzz.Continue) { - // TemplateRef is set to nil by omission; this is required for round trip - c.Fuzz(&j.Template) - c.Fuzz(&j.Selector) - j.Replicas = int(c.RandUint64()) + c.FuzzNoCustom(j) // fuzz self without calling this function again + j.TemplateRef = nil // this is required for round trip }, func(j *api.ReplicationControllerStatus, c fuzz.Continue) { // only replicas round trips j.Replicas = int(c.RandUint64()) }, func(j *api.List, c fuzz.Continue) { - c.Fuzz(&j.ListMeta) - c.Fuzz(&j.Items) + c.FuzzNoCustom(j) // fuzz self without calling this function again if j.Items == nil { j.Items = []runtime.Object{} } @@ -132,18 +132,6 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { *j = t } }, - func(intstr *util.IntOrString, c fuzz.Continue) { - // util.IntOrString will panic if its kind is set wrong. - if c.RandBool() { - intstr.Kind = util.IntstrInt - intstr.IntVal = int(c.RandUint64()) - intstr.StrVal = "" - } else { - intstr.Kind = util.IntstrString - intstr.IntVal = 0 - intstr.StrVal = c.RandString() - } - }, func(pb map[docker.Port][]docker.PortBinding, c fuzz.Continue) { // This is necessary because keys with nil values get omitted. // TODO: Is this a bug? @@ -173,13 +161,13 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { *p = policies[c.Rand.Intn(len(policies))] }, func(rp *api.RestartPolicy, c fuzz.Continue) { - // Exactly one of the fields should be set. - fuzzOneOf(c, &rp.Always, &rp.OnFailure, &rp.Never) + policies := []api.RestartPolicy{api.RestartPolicyAlways, api.RestartPolicyNever, api.RestartPolicyOnFailure} + *rp = policies[c.Rand.Intn(len(policies))] }, func(vs *api.VolumeSource, c fuzz.Continue) { // Exactly one of the fields should be set. //FIXME: the fuzz can still end up nil. What if fuzz allowed me to say that? - fuzzOneOf(c, &vs.HostPath, &vs.EmptyDir, &vs.GCEPersistentDisk, &vs.GitRepo, &vs.Secret) + fuzzOneOf(c, &vs.HostPath, &vs.EmptyDir, &vs.GCEPersistentDisk, &vs.GitRepo, &vs.Secret, &vs.NFS) }, func(d *api.DNSPolicy, c fuzz.Continue) { policies := []api.DNSPolicy{api.DNSClusterFirst, api.DNSDefault} @@ -194,34 +182,12 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { *p = types[c.Rand.Intn(len(types))] }, func(ct *api.Container, c fuzz.Continue) { - // This function exists soley to set TerminationMessagePath to a - // non-empty string. TODO: consider making TerminationMessagePath a - // new type to simplify fuzzing. - ct.TerminationMessagePath = api.TerminationMessagePathDefault - // Let fuzzer handle the rest of the fileds. - c.Fuzz(&ct.Name) - c.Fuzz(&ct.Image) - c.Fuzz(&ct.Command) - c.Fuzz(&ct.Ports) - c.Fuzz(&ct.WorkingDir) - c.Fuzz(&ct.Env) - c.Fuzz(&ct.VolumeMounts) - c.Fuzz(&ct.LivenessProbe) - c.Fuzz(&ct.Lifecycle) - c.Fuzz(&ct.ImagePullPolicy) - c.Fuzz(&ct.Privileged) - c.Fuzz(&ct.Capabilities) + c.FuzzNoCustom(ct) // fuzz self without calling this function again + ct.TerminationMessagePath = "/" + ct.TerminationMessagePath // Must be non-empty }, func(e *api.Event, c fuzz.Continue) { + c.FuzzNoCustom(e) // fuzz self without calling this function again // Fix event count to 1, otherwise, if a v1beta1 or v1beta2 event has a count set arbitrarily, it's count is ignored - c.Fuzz(&e.TypeMeta) - c.Fuzz(&e.ObjectMeta) - c.Fuzz(&e.InvolvedObject) - c.Fuzz(&e.Reason) - c.Fuzz(&e.Message) - c.Fuzz(&e.Source) - c.Fuzz(&e.FirstTimestamp) - c.Fuzz(&e.LastTimestamp) if e.FirstTimestamp.IsZero() { e.Count = 1 } else { @@ -229,17 +195,30 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { } }, func(s *api.Secret, c fuzz.Continue) { - c.Fuzz(&s.TypeMeta) - c.Fuzz(&s.ObjectMeta) - + c.FuzzNoCustom(s) // fuzz self without calling this function again s.Type = api.SecretTypeOpaque - c.Fuzz(&s.Data) + }, + func(s *api.NamespaceStatus, c fuzz.Continue) { + s.Phase = api.NamespaceActive }, func(ep *api.Endpoint, c fuzz.Continue) { // TODO: If our API used a particular type for IP fields we could just catch that here. ep.IP = fmt.Sprintf("%d.%d.%d.%d", c.Rand.Intn(256), c.Rand.Intn(256), c.Rand.Intn(256), c.Rand.Intn(256)) ep.Port = c.Rand.Intn(65536) }, + func(http *api.HTTPGetAction, c fuzz.Continue) { + c.FuzzNoCustom(http) // fuzz self without calling this function again + http.Path = "/" + http.Path // can't be blank + }, + func(ss *api.ServiceSpec, c fuzz.Continue) { + c.FuzzNoCustom(ss) // fuzz self without calling this function again + switch ss.ContainerPort.Kind { + case util.IntstrInt: + ss.ContainerPort.IntVal = 1 + ss.ContainerPort.IntVal%65535 // non-zero + case util.IntstrString: + ss.ContainerPort.StrVal = "x" + ss.ContainerPort.StrVal // non-empty + } + }, ) return f } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/types.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/types.go index 5f8607e40c09..64af4110920b 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/types.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/types.go @@ -120,6 +120,17 @@ type ObjectMeta struct { // Clients may not set this value. It is represented in RFC3339 form and is in UTC. CreationTimestamp util.Time `json:"creationTimestamp,omitempty"` + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *util.Time `json:"deletionTimestamp,omitempty"` + // Labels are key value pairs that may be used to scope and select individual resources. // Label keys are of the form: // label-key ::= prefixed-name | name @@ -155,10 +166,10 @@ type Volume struct { // Required: This must be a DNS_LABEL. Each volume in a pod must have // a unique name. Name string `json:"name"` - // Source represents the location and type of a volume to mount. + // The VolumeSource represents the location and type of a volume to mount. // This is optional for now. If not specified, the Volume is implied to be an EmptyDir. // This implied behavior is deprecated and will be removed in a future version. - Source VolumeSource `json:"source,omitempty"` + VolumeSource `json:",inline,omitempty"` } // VolumeSource represents the source location of a volume to mount. @@ -180,14 +191,33 @@ type VolumeSource struct { GitRepo *GitRepoVolumeSource `json:"gitRepo"` // Secret represents a secret that should populate this volume. Secret *SecretVolumeSource `json:"secret"` + // NFS represents an NFS mount on the host that shares a pod's lifetime + NFS *NFSVolumeSource `json:"nfs"` } -// HostPathVolumeSource represents bare host directory volume. +// HostPathVolumeSource represents a host directory mapped into a pod. type HostPathVolumeSource struct { Path string `json:"path"` } -type EmptyDirVolumeSource struct{} +// EmptyDirVolumeSource represents an empty directory for a pod. +type EmptyDirVolumeSource struct { + // TODO: Longer term we want to represent the selection of underlying + // media more like a scheduling problem - user says what traits they + // need, we give them a backing store that satisifies that. For now + // this will cover the most common needs. + // Optional: what type of storage medium should back this directory. + // The default is "" which means to use the node's default medium. + Medium StorageType `json:"medium"` +} + +// StorageType defines ways that storage can be allocated to a volume. +type StorageType string + +const ( + StorageTypeDefault StorageType = "" // use whatever the default is for the node + StorageTypeMemory StorageType = "Memory" // use memory (tmpfs) +) // Protocol defines network protocols supported for things like conatiner ports. type Protocol string @@ -239,8 +269,21 @@ type SecretVolumeSource struct { Target ObjectReference `json:"target"` } -// Port represents a network port in a single container -type Port struct { +// NFSVolumeSource represents an NFS Mount that lasts the lifetime of a pod +type NFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `json:"server"` + + // Path is the exported NFS share + Path string `json:"path"` + + // Optional: Defaults to false (read/write). ReadOnly here will force + // the NFS export to be mounted with read-only permissions + ReadOnly bool `json:"readOnly,omitempty"` +} + +// ContainerPort represents a network port in a single container +type ContainerPort struct { // Optional: If specified, this must be a DNS_LABEL. Each named port // in a pod must have a unique name. Name string `json:"name,omitempty"` @@ -346,9 +389,9 @@ type Container struct { // Optional: Defaults to whatever is defined in the image. Command []string `json:"command,omitempty"` // Optional: Defaults to Docker's default. - WorkingDir string `json:"workingDir,omitempty"` - Ports []Port `json:"ports,omitempty"` - Env []EnvVar `json:"env,omitempty"` + WorkingDir string `json:"workingDir,omitempty"` + Ports []ContainerPort `json:"ports,omitempty"` + Env []EnvVar `json:"env,omitempty"` // Compute resource requirements. Resources ResourceRequirements `json:"resources,omitempty"` VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` @@ -440,9 +483,6 @@ type ContainerStatus struct { // Note that this is calculated from dead containers. But those containers are subject to // garbage collection. This value will get capped at 5 by GC. RestartCount int `json:"restartCount"` - // TODO(dchen1107): Deprecated this soon once we pull entire PodStatus from node, - // not just PodInfo. Now we need this to remove docker.Container from API - PodIP string `json:"podIP,omitempty"` // TODO(dchen1107): Need to decide how to represent this in v1beta3 Image string `json:"image"` ImageID string `json:"imageID" description:"ID of the container's image"` @@ -472,18 +512,18 @@ const ( PodUnknown PodPhase = "Unknown" ) -type PodConditionKind string +type PodConditionType string // These are valid conditions of pod. const ( // PodReady means the pod is able to service requests and should be added to the // load balancing pools of all matching services. - PodReady PodConditionKind = "Ready" + PodReady PodConditionType = "Ready" ) // TODO: add LastTransitionTime, Reason, Message to match NodeCondition api. type PodCondition struct { - Kind PodConditionKind `json:"kind"` + Type PodConditionType `json:"type"` Status ConditionStatus `json:"status"` } @@ -498,23 +538,17 @@ type PodContainerInfo struct { ContainerInfo PodInfo `json:"containerInfo"` } -type RestartPolicyAlways struct{} - -// TODO(dchen1107): Define what kinds of failures should restart. -// TODO(dchen1107): Decide whether to support policy knobs, and, if so, which ones. -type RestartPolicyOnFailure struct{} - -type RestartPolicyNever struct{} - // RestartPolicy describes how the container should be restarted. // Only one of the following restart policies may be specified. // If none of the following policies is specified, the default one // is RestartPolicyAlways. -type RestartPolicy struct { - Always *RestartPolicyAlways `json:"always,omitempty"` - OnFailure *RestartPolicyOnFailure `json:"onFailure,omitempty"` - Never *RestartPolicyNever `json:"never,omitempty"` -} +type RestartPolicy string + +const ( + RestartPolicyAlways RestartPolicy = "Always" + RestartPolicyOnFailure RestartPolicy = "OnFailure" + RestartPolicyNever RestartPolicy = "Never" +) // PodList is a list of Pods. type PodList struct { @@ -540,7 +574,8 @@ const ( // PodSpec is a description of a pod type PodSpec struct { - Volumes []Volume `json:"volumes"` + Volumes []Volume `json:"volumes"` + // Required: there must be at least one container in a pod. Containers []Container `json:"containers"` RestartPolicy RestartPolicy `json:"restartPolicy,omitempty"` // Required: Set DNS policy. @@ -718,12 +753,18 @@ type ServiceSpec struct { // CreateExternalLoadBalancer indicates whether a load balancer should be created for this service. CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty"` - // PublicIPs are used by external load balancers. + // PublicIPs are used by external load balancers, or can be set by + // users to handle external traffic that arrives at a node. + // For load balancers, the publicIP will usually be the IP address of the load balancer, + // but some load balancers (notably AWS ELB) use a hostname instead of an IP address. + // For hostnames, the user will use a CNAME record (instead of using an A record with the IP) PublicIPs []string `json:"publicIPs,omitempty"` // ContainerPort is the name or number of the port on the container to direct traffic to. // This is useful if the containers the service points to have multiple open ports. // Optional: If unspecified, the first port on the container will be used. + // As of v1beta3 this field will become required in the internal API, + // and the versioned APIs must provide a default value. ContainerPort util.IntOrString `json:"containerPort,omitempty"` // Required: Supports "ClientIP" and "None". Used to maintain session affinity. @@ -764,6 +805,9 @@ type Endpoint struct { // Required: The destination port to access. Port int `json:"port"` + + // Optional: The kubernetes object related to the entry point. + TargetRef *ObjectReference `json:"targetRef,omitempty"` } // EndpointsList is a list of endpoints. @@ -776,21 +820,47 @@ type EndpointsList struct { // NodeSpec describes the attributes that a node is created with. type NodeSpec struct { - // Capacity represents the available resources of a node + // Capacity represents the available resources of a node. Capacity ResourceList `json:"capacity,omitempty"` + // PodCIDR represents the pod IP range assigned to the node // Note: assigning IP ranges to nodes might need to be revisited when we support migratable IPs. - PodCIDR string `json:"cidr,omitempty"` + PodCIDR string `json:"podCIDR,omitempty"` + + // External ID of the node assigned by some machine database (e.g. a cloud provider) + ExternalID string `json:"externalID,omitempty"` + + // Unschedulable controls node schedulability of new pods. By default node is schedulable. + Unschedulable bool `json:"unschedulable,omitempty"` +} + +// NodeSystemInfo is a set of ids/uuids to uniquely identify the node. +type NodeSystemInfo struct { + // MachineID is the machine-id reported by the node + MachineID string `json:"machineID"` + // SystemUUID is the system-uuid reported by the node + SystemUUID string `json:"systemUUID"` } // NodeStatus is information about the current status of a node. type NodeStatus struct { - // Queried from cloud provider, if available. - HostIP string `json:"hostIP,omitempty"` // NodePhase is the current lifecycle phase of the node. Phase NodePhase `json:"phase,omitempty"` // Conditions is an array of current node conditions. Conditions []NodeCondition `json:"conditions,omitempty"` + // Queried from cloud provider, if available. + Addresses []NodeAddress `json:"addresses,omitempty"` + // NodeSystemInfo is a set of ids/uuids to uniquely identify the node + NodeInfo NodeSystemInfo `json:"nodeInfo,omitempty"` +} + +// NodeInfo is the information collected on the node. +type NodeInfo struct { + TypeMeta `json:",inline"` + // Capacity represents the available resources of a node + Capacity ResourceList `json:"capacity,omitempty"` + // NodeSystemInfo is a set of ids/uuids to uniquely identify the node + NodeSystemInfo `json:",inline,omitempty"` } type NodePhase string @@ -805,20 +875,22 @@ const ( NodeTerminated NodePhase = "Terminated" ) -type NodeConditionKind string +type NodeConditionType string // These are valid conditions of node. Currently, we don't have enough information to decide // node condition. In the future, we will add more. The proposed set of conditions are: // NodeReachable, NodeLive, NodeReady, NodeSchedulable, NodeRunnable. const ( // NodeReachable means the node can be reached (in the sense of HTTP connection) from node controller. - NodeReachable NodeConditionKind = "Reachable" + NodeReachable NodeConditionType = "Reachable" // NodeReady means the node returns StatusOK for HTTP health check. - NodeReady NodeConditionKind = "Ready" + NodeReady NodeConditionType = "Ready" + // NodeSchedulable means the node is ready to accept new pods. + NodeSchedulable NodeConditionType = "Schedulable" ) type NodeCondition struct { - Kind NodeConditionKind `json:"kind"` + Type NodeConditionType `json:"type"` Status ConditionStatus `json:"status"` LastProbeTime util.Time `json:"lastProbeTime,omitempty"` LastTransitionTime util.Time `json:"lastTransitionTime,omitempty"` @@ -826,6 +898,22 @@ type NodeCondition struct { Message string `json:"message,omitempty"` } +type NodeAddressType string + +// These are valid address types of node. NodeLegacyHostIP is used to transit +// from out-dated HostIP field to NodeAddress. +const ( + NodeLegacyHostIP NodeAddressType = "LegacyHostIP" + NodeHostName NodeAddressType = "Hostname" + NodeExternalIP NodeAddressType = "ExternalIP" + NodeInternalIP NodeAddressType = "InternalIP" +) + +type NodeAddress struct { + Type NodeAddressType `json:"type"` + Address string `json:"address"` +} + // NodeResources is an object for conveying resource information about a node. // see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md for more details. // TODO: Use ResourceList instead? @@ -860,7 +948,7 @@ type Node struct { Status NodeStatus `json:"status,omitempty"` } -// NodeList is a list of minions. +// NodeList is a list of nodes. type NodeList struct { TypeMeta `json:",inline"` ListMeta `json:"metadata,omitempty"` @@ -874,8 +962,20 @@ type NamespaceSpec struct { // NamespaceStatus is information about the current status of a Namespace. type NamespaceStatus struct { + // Phase is the current lifecycle phase of the namespace. + Phase NamespacePhase `json:"phase,omitempty"` } +type NamespacePhase string + +// These are the valid phases of a namespace. +const ( + // NamespaceActive means the namespace is available for use in the system + NamespaceActive NamespacePhase = "Active" + // NamespaceTerminating means the namespace is undergoing graceful termination + NamespaceTerminating NamespacePhase = "Terminating" +) + // A namespace provides a scope for Names. // Use of multiple namespaces is optional type Namespace struct { @@ -897,13 +997,24 @@ type NamespaceList struct { Items []Namespace `json:"items"` } -// Binding is written by a scheduler to cause a pod to be bound to a host. +// Binding ties one object to another - for example, a pod is bound to a node by a scheduler. type Binding struct { - TypeMeta `json:",inline"` + TypeMeta `json:",inline"` + // ObjectMeta describes the object that is being bound. ObjectMeta `json:"metadata,omitempty"` - PodID string `json:"podID"` - Host string `json:"host"` + // Target is the object to bind to. + Target ObjectReference `json:"target"` +} + +// DeleteOptions may be provided when deleting an API object +type DeleteOptions struct { + TypeMeta `json:",inline"` + + // Optional duration in seconds before the object should be deleted. Value must be non-negative integer. + // The value zero indicates delete immediately. If this value is nil, the default grace period for the + // specified type will be used. + GracePeriodSeconds *int64 `json:"gracePeriodSeconds"` } // Status is a return value for calls that don't return other objects. @@ -1166,7 +1277,7 @@ type EventList struct { // ContainerManifest corresponds to the Container Manifest format, documented at: // https://developers.google.com/compute/docs/containers/container_vms#container_manifest // This is used as the representation of Kubernetes workloads. -// DEPRECATED: Replaced with BoundPod +// DEPRECATED: Replaced with Pod type ContainerManifest struct { // Required: This must be a supported version string, such as "v1beta1". Version string `json:"version"` @@ -1185,7 +1296,7 @@ type ContainerManifest struct { } // ContainerManifestList is used to communicate container manifests to kubelet. -// DEPRECATED: Replaced with BoundPods +// DEPRECATED: Replaced with Pods type ContainerManifestList struct { TypeMeta `json:",inline"` ListMeta `json:"metadata,omitempty"` @@ -1193,30 +1304,6 @@ type ContainerManifestList struct { Items []ContainerManifest `json:"items"` } -// BoundPod is a collection of containers that should be run on a host. A BoundPod -// defines how a Pod may change after a Binding is created. A Pod is a request to -// execute a pod, whereas a BoundPod is the specification that would be run on a server. -type BoundPod struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` - - // Spec defines the behavior of a pod. - Spec PodSpec `json:"spec,omitempty"` -} - -// BoundPods is a list of Pods bound to a common server. The resource version of -// the pod list is guaranteed to only change when the list of bound pods changes. -type BoundPods struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` - - // Host is the name of a node that these pods were bound to. - Host string `json:"host"` - - // Items is the list of all pods bound to a given host. - Items []BoundPod `json:"items"` -} - // List holds a list of objects, which may not be known by the server. type List struct { TypeMeta `json:",inline"` @@ -1307,16 +1394,6 @@ type ResourceQuota struct { Status ResourceQuotaStatus `json:"status,omitempty"` } -// ResourceQuotaUsage captures system observed quota status per namespace -// It is used to enforce atomic updates of a backing ResourceQuota.Status field in storage -type ResourceQuotaUsage struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` - - // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty"` -} - // ResourceQuotaList is a list of ResourceQuota items type ResourceQuotaList struct { TypeMeta `json:",inline"` @@ -1384,3 +1461,19 @@ const ( PortHeader = "port" ) + +// Appends the NodeAddresses to the passed-by-pointer slice, only if they do not already exist +func AddToNodeAddresses(addresses *[]NodeAddress, addAddresses ...NodeAddress) { + for _, add := range addAddresses { + exists := false + for _, existing := range *addresses { + if existing.Address == add.Address && existing.Type == add.Type { + exists = true + break + } + } + if !exists { + *addresses = append(*addresses, add) + } + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/unversioned.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/unversioned.go index c1604b5612f8..5792df4c1988 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/unversioned.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/unversioned.go @@ -16,6 +16,10 @@ limitations under the License. package api +import ( + "strings" +) + // This file contains API types that are unversioned. // APIVersions lists the api versions that are available, to allow @@ -30,3 +34,31 @@ type APIVersions struct { type RootPaths struct { Paths []string `json:"paths"` } + +// preV1Beta3 returns true if the provided API version is an API introduced before v1beta3. +func PreV1Beta3(version string) bool { + return version == "v1beta1" || version == "v1beta2" +} + +func LabelSelectorQueryParam(version string) string { + if PreV1Beta3(version) { + return "labels" + } + return "label-selector" +} + +func FieldSelectorQueryParam(version string) string { + if PreV1Beta3(version) { + return "fields" + } + return "field-selector" +} + +// String returns available api versions as a human-friendly version string. +func (apiVersions APIVersions) String() string { + return strings.Join(apiVersions.Versions, ",") +} + +func (apiVersions APIVersions) GoString() string { + return apiVersions.String() +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/conversion.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/conversion.go index 6ba485bdbe9b..99a54d5bdb67 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/conversion.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/conversion.go @@ -36,6 +36,7 @@ func init() { newer.Scheme.AddStructFieldConversion(newer.TypeMeta{}, "TypeMeta", TypeMeta{}, "TypeMeta") newer.Scheme.AddStructFieldConversion(newer.ObjectMeta{}, "ObjectMeta", TypeMeta{}, "TypeMeta") newer.Scheme.AddStructFieldConversion(newer.ListMeta{}, "ListMeta", TypeMeta{}, "TypeMeta") + newer.Scheme.AddStructFieldConversion(newer.Endpoints{}, "Endpoints", Endpoints{}, "Endpoints") // TODO: scope this to a specific type once that becomes available and remove the Event conversion functions below // newer.Scheme.AddStructFieldConversion(string(""), "Status", string(""), "Condition") @@ -83,6 +84,7 @@ func init() { out.GenerateName = in.GenerateName out.UID = in.UID out.CreationTimestamp = in.CreationTimestamp + out.DeletionTimestamp = in.DeletionTimestamp out.SelfLink = in.SelfLink if len(in.ResourceVersion) > 0 { v, err := strconv.ParseUint(in.ResourceVersion, 10, 64) @@ -99,6 +101,7 @@ func init() { out.GenerateName = in.GenerateName out.UID = in.UID out.CreationTimestamp = in.CreationTimestamp + out.DeletionTimestamp = in.DeletionTimestamp out.SelfLink = in.SelfLink if in.ResourceVersion != 0 { out.ResourceVersion = strconv.FormatUint(in.ResourceVersion, 10) @@ -447,18 +450,6 @@ func init() { return nil }, - func(in *newer.PodSpec, out *BoundPod, s conversion.Scope) error { - if err := s.Convert(&in, &out.Spec, 0); err != nil { - return err - } - return nil - }, - func(in *BoundPod, out *newer.PodSpec, s conversion.Scope) error { - if err := s.Convert(&in.Spec, &out, 0); err != nil { - return err - } - return nil - }, // Converts internal Container to v1beta1.Container. // Fields 'CPU' and 'Memory' are not present in the internal Container object. // Hence the need for a custom conversion function. @@ -696,9 +687,21 @@ func init() { if err := s.Convert(&in.Status.Conditions, &out.Status.Conditions, 0); err != nil { return err } + if err := s.Convert(&in.Status.Addresses, &out.Status.Addresses, 0); err != nil { + return err + } + if err := s.Convert(&in.Status.NodeInfo, &out.Status.NodeInfo, 0); err != nil { + return err + } - out.HostIP = in.Status.HostIP + for _, address := range in.Status.Addresses { + if address.Type == newer.NodeLegacyHostIP { + out.HostIP = address.Address + } + } out.PodCIDR = in.Spec.PodCIDR + out.ExternalID = in.Spec.ExternalID + out.Unschedulable = in.Spec.Unschedulable return s.Convert(&in.Spec.Capacity, &out.NodeResources.Capacity, 0) }, func(in *Minion, out *newer.Node, s conversion.Scope) error { @@ -717,11 +720,23 @@ func init() { if err := s.Convert(&in.Status.Conditions, &out.Status.Conditions, 0); err != nil { return err } + if err := s.Convert(&in.Status.Addresses, &out.Status.Addresses, 0); err != nil { + return err + } + if err := s.Convert(&in.Status.NodeInfo, &out.Status.NodeInfo, 0); err != nil { + return err + } - out.Status.HostIP = in.HostIP + if in.HostIP != "" { + newer.AddToNodeAddresses(&out.Status.Addresses, + newer.NodeAddress{Type: newer.NodeLegacyHostIP, Address: in.HostIP}) + } out.Spec.PodCIDR = in.PodCIDR + out.Spec.ExternalID = in.ExternalID + out.Spec.Unschedulable = in.Unschedulable return s.Convert(&in.NodeResources.Capacity, &out.Spec.Capacity, 0) }, + func(in *newer.LimitRange, out *LimitRange, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -746,6 +761,7 @@ func init() { } return nil }, + func(in *Namespace, out *newer.Namespace, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -756,11 +772,15 @@ func init() { if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { return err } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } if err := s.Convert(&in.Labels, &out.ObjectMeta.Labels, 0); err != nil { return err } return nil }, + func(in *newer.LimitRangeSpec, out *LimitRangeSpec, s conversion.Scope) error { *out = LimitRangeSpec{} out.Limits = make([]LimitRangeItem, len(in.Limits), len(in.Limits)) @@ -781,6 +801,7 @@ func init() { } return nil }, + func(in *newer.LimitRangeItem, out *LimitRangeItem, s conversion.Scope) error { *out = LimitRangeItem{} out.Type = LimitType(in.Type) @@ -803,6 +824,7 @@ func init() { } return nil }, + func(in *newer.ResourceQuota, out *ResourceQuota, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -816,6 +838,9 @@ func init() { if err := s.Convert(&in.Status, &out.Status, 0); err != nil { return err } + if err := s.Convert(&in.Labels, &out.Labels, 0); err != nil { + return err + } return nil }, func(in *ResourceQuota, out *newer.ResourceQuota, s conversion.Scope) error { @@ -831,32 +856,12 @@ func init() { if err := s.Convert(&in.Status, &out.Status, 0); err != nil { return err } - return nil - }, - func(in *newer.ResourceQuotaUsage, out *ResourceQuotaUsage, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { - return err - } - return nil - }, - func(in *ResourceQuotaUsage, out *newer.ResourceQuotaUsage, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.TypeMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + if err := s.Convert(&in.Labels, &out.ObjectMeta.Labels, 0); err != nil { return err } return nil }, + func(in *newer.ResourceQuotaSpec, out *ResourceQuotaSpec, s conversion.Scope) error { *out = ResourceQuotaSpec{} if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { @@ -871,6 +876,7 @@ func init() { } return nil }, + func(in *newer.ResourceQuotaStatus, out *ResourceQuotaStatus, s conversion.Scope) error { *out = ResourceQuotaStatus{} if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { @@ -891,6 +897,7 @@ func init() { } return nil }, + // Object ID <-> Name // TODO: amend the conversion package to allow overriding specific fields. func(in *ObjectReference, out *newer.ObjectReference, s conversion.Scope) error { @@ -1014,6 +1021,21 @@ func init() { return nil }, + func(in *newer.Volume, out *Volume, s conversion.Scope) error { + if err := s.Convert(&in.VolumeSource, &out.Source, 0); err != nil { + return err + } + out.Name = in.Name + return nil + }, + func(in *Volume, out *newer.Volume, s conversion.Scope) error { + if err := s.Convert(&in.Source, &out.VolumeSource, 0); err != nil { + return err + } + out.Name = in.Name + return nil + }, + // VolumeSource's HostDir is deprecated in favor of HostPath. // TODO: It would be great if I could just map field names to // convert or else maybe say "convert all members of this @@ -1034,6 +1056,9 @@ func init() { if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil { return err } + if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { + return err + } return nil }, func(in *VolumeSource, out *newer.VolumeSource, s conversion.Scope) error { @@ -1052,6 +1077,9 @@ func init() { if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil { return err } + if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { + return err + } return nil }, @@ -1088,6 +1116,33 @@ func init() { return nil }, + func(in *newer.RestartPolicy, out *RestartPolicy, s conversion.Scope) error { + switch *in { + case newer.RestartPolicyAlways: + *out = RestartPolicy{Always: &RestartPolicyAlways{}} + case newer.RestartPolicyNever: + *out = RestartPolicy{Never: &RestartPolicyNever{}} + case newer.RestartPolicyOnFailure: + *out = RestartPolicy{OnFailure: &RestartPolicyOnFailure{}} + default: + *out = RestartPolicy{} + } + return nil + }, + func(in *RestartPolicy, out *newer.RestartPolicy, s conversion.Scope) error { + switch { + case in.Always != nil: + *out = newer.RestartPolicyAlways + case in.Never != nil: + *out = newer.RestartPolicyNever + case in.OnFailure != nil: + *out = newer.RestartPolicyOnFailure + default: + *out = "" + } + return nil + }, + func(in *newer.Probe, out *LivenessProbe, s conversion.Scope) error { if err := s.Convert(&in.Exec, &out.Exec, 0); err != nil { return err @@ -1116,6 +1171,7 @@ func init() { out.TimeoutSeconds = in.TimeoutSeconds return nil }, + func(in *newer.Endpoints, out *Endpoints, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -1128,7 +1184,17 @@ func init() { } for i := range in.Endpoints { ep := &in.Endpoints[i] - out.Endpoints = append(out.Endpoints, net.JoinHostPort(ep.IP, strconv.Itoa(ep.Port))) + hostPort := net.JoinHostPort(ep.IP, strconv.Itoa(ep.Port)) + out.Endpoints = append(out.Endpoints, hostPort) + if ep.TargetRef != nil { + target := EndpointObjectReference{ + Endpoint: hostPort, + } + if err := s.Convert(ep.TargetRef, &target.ObjectReference, 0); err != nil { + return err + } + out.TargetRefs = append(out.TargetRefs, target) + } } return nil }, @@ -1155,12 +1221,209 @@ func init() { return err } ep.Port = pn + for j := range in.TargetRefs { + if in.TargetRefs[j].Endpoint != in.Endpoints[i] { + continue + } + ep.TargetRef = &newer.ObjectReference{} + if err := s.Convert(&in.TargetRefs[j].ObjectReference, ep.TargetRef, 0); err != nil { + return err + } + } } return nil }, + + func(in *newer.NodeCondition, out *NodeCondition, s conversion.Scope) error { + if err := s.Convert(&in.Type, &out.Kind, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + if err := s.Convert(&in.LastProbeTime, &out.LastProbeTime, 0); err != nil { + return err + } + if err := s.Convert(&in.LastTransitionTime, &out.LastTransitionTime, 0); err != nil { + return err + } + if err := s.Convert(&in.Reason, &out.Reason, 0); err != nil { + return err + } + if err := s.Convert(&in.Message, &out.Message, 0); err != nil { + return err + } + return nil + }, + func(in *NodeCondition, out *newer.NodeCondition, s conversion.Scope) error { + if err := s.Convert(&in.Kind, &out.Type, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + if err := s.Convert(&in.LastProbeTime, &out.LastProbeTime, 0); err != nil { + return err + } + if err := s.Convert(&in.LastTransitionTime, &out.LastTransitionTime, 0); err != nil { + return err + } + if err := s.Convert(&in.Reason, &out.Reason, 0); err != nil { + return err + } + if err := s.Convert(&in.Message, &out.Message, 0); err != nil { + return err + } + return nil + }, + + func(in *newer.NodeConditionType, out *NodeConditionKind, s conversion.Scope) error { + switch *in { + case newer.NodeReachable: + *out = NodeReachable + break + case newer.NodeReady: + *out = NodeReady + break + case "": + *out = "" + default: + *out = NodeConditionKind(*in) + break + } + + return nil + }, + func(in *NodeConditionKind, out *newer.NodeConditionType, s conversion.Scope) error { + switch *in { + case NodeReachable: + *out = newer.NodeReachable + break + case NodeReady: + *out = newer.NodeReady + break + case "": + *out = "" + default: + *out = newer.NodeConditionType(*in) + break + } + + return nil + }, + + func(in *newer.PodCondition, out *PodCondition, s conversion.Scope) error { + if err := s.Convert(&in.Type, &out.Kind, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil + }, + func(in *PodCondition, out *newer.PodCondition, s conversion.Scope) error { + if err := s.Convert(&in.Kind, &out.Type, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil + }, + + func(in *newer.PodConditionType, out *PodConditionKind, s conversion.Scope) error { + switch *in { + case newer.PodReady: + *out = PodReady + break + case "": + *out = "" + default: + *out = PodConditionKind(*in) + break + } + + return nil + }, + func(in *PodConditionKind, out *newer.PodConditionType, s conversion.Scope) error { + switch *in { + case PodReady: + *out = newer.PodReady + break + case "": + *out = "" + default: + *out = newer.PodConditionType(*in) + break + } + + return nil + }, + func(in *Binding, out *newer.Binding, s conversion.Scope) error { + if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil { + return err + } + out.Target = newer.ObjectReference{ + Name: in.Host, + } + out.Name = in.PodID + return nil + }, + func(in *newer.Binding, out *Binding, s conversion.Scope) error { + if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil { + return err + } + out.Host = in.Target.Name + out.PodID = in.Name + return nil + }, ) if err != nil { // If one of the conversion functions is malformed, detect it immediately. panic(err) } + + // Add field conversion funcs. + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "pods", + func(label, value string) (string, string, error) { + switch label { + case "name": + return "name", value, nil + case "DesiredState.Host": + return "spec.host", value, nil + case "DesiredState.Status": + podStatus := PodStatus(value) + var internalValue newer.PodPhase + newer.Scheme.Convert(&podStatus, &internalValue) + return "status.phase", string(internalValue), nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "events", + func(label, value string) (string, string, error) { + switch label { + case "involvedObject.kind", + "involvedObject.namespace", + "involvedObject.uid", + "involvedObject.apiVersion", + "involvedObject.resourceVersion", + "involvedObject.fieldPath", + "reason", + "source": + return label, value, nil + case "involvedObject.id": + return "involvedObject.name", value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/conversion_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/conversion_test.go index df0009b41982..b0dec254d7f6 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/conversion_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/conversion_test.go @@ -29,6 +29,17 @@ import ( var Convert = newer.Scheme.Convert +func TestEmptyObjectConversion(t *testing.T) { + s, err := current.Codec.Encode(¤t.LimitRange{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // DeletionTimestamp is not included, while CreationTimestamp is (would always be set) + if string(s) != `{"kind":"LimitRange","creationTimestamp":null,"apiVersion":"v1beta1","spec":{"limits":null}}` { + t.Errorf("unexpected empty object: %s", string(s)) + } +} + func TestNodeConversion(t *testing.T) { version, kind, err := newer.Scheme.ObjectVersionAndKind(¤t.Minion{}) if err != nil { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/defaults.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/defaults.go index 7c9042dea329..95f63c226338 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/defaults.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/defaults.go @@ -32,7 +32,7 @@ func init() { } } }, - func(obj *Port) { + func(obj *ContainerPort) { if obj.Protocol == "" { obj.Protocol = ProtocolTCP } @@ -90,5 +90,15 @@ func init() { obj.Protocol = "TCP" } }, + func(obj *HTTPGetAction) { + if obj.Path == "" { + obj.Path = "/" + } + }, + func(obj *NamespaceStatus) { + if obj.Phase == "" { + obj.Phase = NamespaceActive + } + }, ) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/defaults_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/defaults_test.go index d7372045247c..a94642654067 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/defaults_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/defaults_test.go @@ -51,48 +51,6 @@ func TestSetDefaultService(t *testing.T) { } } -func TestSetDefaulPodSpec(t *testing.T) { - bp := ¤t.BoundPod{} - bp.Spec.Volumes = []current.Volume{{}} - - obj2 := roundTrip(t, runtime.Object(bp)) - bp2 := obj2.(*current.BoundPod) - if bp2.Spec.DNSPolicy != current.DNSClusterFirst { - t.Errorf("Expected default dns policy :%s, got: %s", current.DNSClusterFirst, bp2.Spec.DNSPolicy) - } - policy := bp2.Spec.RestartPolicy - if policy.Never != nil || policy.OnFailure != nil || policy.Always == nil { - t.Errorf("Expected only policy.Always is set, got: %s", policy) - } - vsource := bp2.Spec.Volumes[0].Source - if vsource.EmptyDir == nil { - t.Errorf("Expected non-empty volume is set, got: %s", vsource.EmptyDir) - } -} - -func TestSetDefaultContainer(t *testing.T) { - bp := ¤t.BoundPod{} - bp.Spec.Containers = []current.Container{{}} - bp.Spec.Containers[0].Ports = []current.Port{{}} - - obj2 := roundTrip(t, runtime.Object(bp)) - bp2 := obj2.(*current.BoundPod) - - container := bp2.Spec.Containers[0] - if container.TerminationMessagePath != current.TerminationMessagePathDefault { - t.Errorf("Expected termination message path: %s, got: %s", - current.TerminationMessagePathDefault, container.TerminationMessagePath) - } - if container.ImagePullPolicy != current.PullIfNotPresent { - t.Errorf("Expected image pull policy: %s, got: %s", - current.PullIfNotPresent, container.ImagePullPolicy) - } - if container.Ports[0].Protocol != current.ProtocolTCP { - t.Errorf("Expected protocol: %s, got: %s", - current.ProtocolTCP, container.Ports[0].Protocol) - } -} - func TestSetDefaultSecret(t *testing.T) { s := ¤t.Secret{} obj2 := roundTrip(t, runtime.Object(s)) @@ -112,3 +70,13 @@ func TestSetDefaulEndpointsProtocol(t *testing.T) { t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Protocol) } } + +func TestSetDefaultNamespace(t *testing.T) { + s := ¤t.Namespace{} + obj2 := roundTrip(t, runtime.Object(s)) + s2 := obj2.(*current.Namespace) + + if s2.Status.Phase != current.NamespaceActive { + t.Errorf("Expected phase %v, got %v", current.NamespaceActive, s2.Status.Phase) + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/register.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/register.go index f61f806f08af..5765cbf7554b 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/register.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/register.go @@ -24,6 +24,12 @@ import ( // Codec encodes internal objects to the v1beta1 scheme var Codec = runtime.CodecFor(api.Scheme, "v1beta1") +// Dependency does nothing but give a hook for other packages to force a +// compile-time error when this API version is eventually removed. This is +// useful, for example, to clean up things that are implicitly tied to +// semantics of older APIs. +const Dependency = true + func init() { api.Scheme.AddKnownTypes("v1beta1", &Pod{}, @@ -37,24 +43,23 @@ func init() { &EndpointsList{}, &Minion{}, &MinionList{}, + &NodeInfo{}, &Binding{}, &Status{}, &Event{}, &EventList{}, &ContainerManifest{}, &ContainerManifestList{}, - &BoundPod{}, - &BoundPods{}, &List{}, &LimitRange{}, &LimitRangeList{}, &ResourceQuota{}, &ResourceQuotaList{}, - &ResourceQuotaUsage{}, &Namespace{}, &NamespaceList{}, &Secret{}, &SecretList{}, + &DeleteOptions{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) @@ -71,6 +76,7 @@ func (*ServiceList) IsAnAPIObject() {} func (*Endpoints) IsAnAPIObject() {} func (*EndpointsList) IsAnAPIObject() {} func (*Minion) IsAnAPIObject() {} +func (*NodeInfo) IsAnAPIObject() {} func (*MinionList) IsAnAPIObject() {} func (*Binding) IsAnAPIObject() {} func (*Status) IsAnAPIObject() {} @@ -78,15 +84,13 @@ func (*Event) IsAnAPIObject() {} func (*EventList) IsAnAPIObject() {} func (*ContainerManifest) IsAnAPIObject() {} func (*ContainerManifestList) IsAnAPIObject() {} -func (*BoundPod) IsAnAPIObject() {} -func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} -func (*ResourceQuotaUsage) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/types.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/types.go index 9e2bd555a689..e1bc92c0899e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/types.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1/types.go @@ -53,13 +53,13 @@ type ContainerManifest struct { Version string `json:"version" description:"manifest version; must be v1beta1"` // Required: This must be a DNS_SUBDOMAIN. // TODO: ID on Manifest is deprecated and will be removed in the future. - ID string `json:"id" description:"manifest name; must be a DNS_SUBDOMAIN"` + ID string `json:"id" description:"manifest name; must be a DNS_SUBDOMAIN; cannot be updated"` // TODO: UUID on Manifext is deprecated in the future once we are done // with the API refactory. It is required for now to determine the instance // of a Pod. - UUID types.UID `json:"uuid,omitempty" description:"manifest UUID"` + UUID types.UID `json:"uuid,omitempty" description:"manifest UUID, populated by the system, read-only"` Volumes []Volume `json:"volumes" description:"list of volumes that can be mounted by containers belonging to the pod"` - Containers []Container `json:"containers" description:"list of containers belonging to the pod"` + Containers []Container `json:"containers" description:"list of containers belonging to the pod; containers cannot currently be added or removed"` RestartPolicy RestartPolicy `json:"restartPolicy,omitempty" description:"restart policy for all containers within the pod; one of RestartPolicyAlways, RestartPolicyOnFailure, RestartPolicyNever"` // Optional: Set DNS policy. Defaults to "ClusterFirst" DNSPolicy DNSPolicy `json:"dnsPolicy,omitempty" description:"DNS policy for containers within the pod; one of 'ClusterFirst' or 'Default'"` @@ -105,6 +105,8 @@ type VolumeSource struct { GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` // Secret represents a secret to populate the volume with Secret *SecretVolumeSource `json:"secret" description:"secret to populate volume with"` + // NFS represents an NFS mount on the host that shares a pod's lifetime + NFS *NFSVolumeSource `json:"nfs" description:"NFS volume that will be mounted in the host machine "` } // HostPathVolumeSource represents bare host directory volume. @@ -112,7 +114,19 @@ type HostPathVolumeSource struct { Path string `json:"path" description:"path of the directory on the host"` } -type EmptyDirVolumeSource struct{} +type EmptyDirVolumeSource struct { + // Optional: what type of storage medium should back this directory. + // The default is "" which means to use the node's default medium. + Medium StorageType `json:"medium" description:"type of storage used to back the volume; must be an empty string (default) or Memory"` +} + +// StorageType defines ways that storage can be allocated to a volume. +type StorageType string + +const ( + StorageTypeDefault StorageType = "" // use whatever the default is for the node + StorageTypeMemory StorageType = "Memory" // use memory (tmpfs) +) // Protocol defines network protocols supported for things like conatiner ports. type Protocol string @@ -161,8 +175,8 @@ type SecretVolumeSource struct { Target ObjectReference `json:"target" description:"target is a reference to a secret"` } -// Port represents a network port in a single container -type Port struct { +// ContainerPort represents a network port in a single container +type ContainerPort struct { // Optional: If specified, this must be a DNS_LABEL. Each named port // in a pod must have a unique name. Name string `json:"name,omitempty" description:"name for the port that can be referred to by services; must be a DNS_LABEL and unique without the pod"` @@ -276,32 +290,32 @@ type ResourceRequirements struct { type Container struct { // Required: This must be a DNS_LABEL. Each container in a pod must // have a unique name. - Name string `json:"name" description:"name of the container; must be a DNS_LABEL and unique within the pod"` + Name string `json:"name" description:"name of the container; must be a DNS_LABEL and unique within the pod; cannot be updated"` // Required. Image string `json:"image" description:"Docker image name"` // Optional: Defaults to whatever is defined in the image. - Command []string `json:"command,omitempty" description:"command argv array; not executed within a shell; defaults to entrypoint or command in the image"` + Command []string `json:"command,omitempty" description:"command argv array; not executed within a shell; defaults to entrypoint or command in the image; cannot be updated"` // Optional: Defaults to Docker's default. - WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default"` - Ports []Port `json:"ports,omitempty" description:"list of ports to expose from the container"` - Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container"` - Resources ResourceRequirements `json:"resources,omitempty" description:"Compute Resources required by this container"` + WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default; cannot be updated"` + Ports []ContainerPort `json:"ports,omitempty" description:"list of ports to expose from the container; cannot be updated"` + Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container; cannot be updated"` + Resources ResourceRequirements `json:"resources,omitempty" description:"Compute Resources required by this container; cannot be updated"` // Optional: Defaults to unlimited. - CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core"` + CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core; cannot be updated"` // Optional: Defaults to unlimited. - Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` - VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesystem"` - LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty" description:"periodic probe of container liveness; container will be restarted if the probe fails"` - ReadinessProbe *LivenessProbe `json:"readinessProbe,omitempty" description:"periodic probe of container service readiness; container will be removed from service endpoints if the probe fails"` - Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events"` + Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited; cannot be updated"` + VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesystem; cannot be updated"` + LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty" description:"periodic probe of container liveness; container will be restarted if the probe fails; cannot be updated"` + ReadinessProbe *LivenessProbe `json:"readinessProbe,omitempty" description:"periodic probe of container service readiness; container will be removed from service endpoints if the probe fails; cannot be updated"` + Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log - TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log"` + TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false"` + Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` // Optional: Policy for pulling images for this container - ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise"` + ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container"` + Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` } // Handler defines a specific action that should be taken @@ -333,14 +347,25 @@ type Lifecycle struct { // TypeMeta is shared by all objects sent to, or returned from the client. type TypeMeta struct { - Kind string `json:"kind,omitempty" description:"kind of object, in CamelCase"` - ID string `json:"id,omitempty" description:"name of the object; must be a DNS_SUBDOMAIN and unique among all objects of the same kind within the same namespace; used in resource URLs"` - UID types.UID `json:"uid,omitempty" description:"UUID assigned by the system upon creation, unique across space and time"` - CreationTimestamp util.Time `json:"creationTimestamp,omitempty" description:"RFC 3339 date and time at which the object was created; recorded by the system; null for lists"` - SelfLink string `json:"selfLink,omitempty" description:"URL for the object"` - ResourceVersion uint64 `json:"resourceVersion,omitempty" description:"string that identifies the internal version of this object that can be used by clients to determine when objects have changed; value must be treated as opaque by clients and passed unmodified back to the server"` + Kind string `json:"kind,omitempty" description:"kind of object, in CamelCase; cannot be updated"` + ID string `json:"id,omitempty" description:"name of the object; must be a DNS_SUBDOMAIN and unique among all objects of the same kind within the same namespace; used in resource URLs; cannot be updated"` + UID types.UID `json:"uid,omitempty" description:"unique UUID across space and time; populated by the system, read-only; cannot be updated"` + CreationTimestamp util.Time `json:"creationTimestamp,omitempty" description:"RFC 3339 date and time at which the object was created; populated by the system, read-only; null for lists"` + SelfLink string `json:"selfLink,omitempty" description:"URL for the object; populated by the system, read-only"` + ResourceVersion uint64 `json:"resourceVersion,omitempty" description:"string that identifies the internal version of this object that can be used by clients to determine when objects have changed; populated by the system, read-only; value must be treated as opaque by clients and passed unmodified back to the server: https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#concurrency-control-and-consistency"` APIVersion string `json:"apiVersion,omitempty" description:"version of the schema the object should have"` - Namespace string `json:"namespace,omitempty" description:"namespace to which the object belongs; must be a DNS_SUBDOMAIN; 'default' by default"` + Namespace string `json:"namespace,omitempty" description:"namespace to which the object belongs; must be a DNS_SUBDOMAIN; 'default' by default; cannot be updated"` + + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *util.Time `json:"deletionTimestamp,omitempty" description:"RFC 3339 date and time at which the object will be deleted; populated by the system when a graceful deletion is requested, read-only; if not set, graceful deletion of the object has not been requested"` // GenerateName indicates that the name should be made unique by the server prior to persisting // it. A non-empty value for the field indicates the name will be made unique (and the name @@ -425,9 +450,6 @@ type ContainerStatus struct { // Note that this is calculated from dead containers. But those containers are subject to // garbage collection. This value will get capped at 5 by GC. RestartCount int `json:"restartCount" description:"the number of times the container has been restarted, currently based on the number of dead containers that have not yet been removed"` - // TODO(dchen1107): Deprecated this soon once we pull entire PodStatus from node, - // not just PodInfo. Now we need this to remove docker.Container from API - PodIP string `json:"podIP,omitempty" description:"pod's IP address"` // TODO(dchen1107): Need to decide how to reprensent this in v1beta3 Image string `json:"image" description:"image of the container"` ImageID string `json:"imageID" description:"ID of the container's image"` @@ -485,7 +507,7 @@ type PodState struct { Conditions []PodCondition `json:"Condition,omitempty" description:"current service state of pod"` // A human readable message indicating details about why the pod is in this state. Message string `json:"message,omitempty" description:"human readable message indicating details about why the pod is in this condition"` - Host string `json:"host,omitempty" description:"host to which the pod is assigned; empty if not yet scheduled"` + Host string `json:"host,omitempty" description:"host to which the pod is assigned; empty if not yet scheduled; cannot be updated"` HostIP string `json:"hostIP,omitempty" description:"IP address of the host to which the pod is assigned; empty if not yet scheduled"` PodIP string `json:"podIP,omitempty" description:"IP address allocated to the pod; routable at least within the cluster; empty if not yet allocated"` @@ -511,7 +533,7 @@ type Pod struct { TypeMeta `json:",inline"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"` DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"` - CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod"` + CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"` // NodeSelector is a selector which must be true for the pod to fit on a node NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` } @@ -533,7 +555,7 @@ type ReplicationControllerList struct { type ReplicationController struct { TypeMeta `json:",inline"` DesiredState ReplicationControllerState `json:"desiredState,omitempty" description:"specification of the desired state of the replication controller"` - CurrentState ReplicationControllerState `json:"currentState,omitempty" description:"current state of the replication controller"` + CurrentState ReplicationControllerState `json:"currentState,omitempty" description:"current state of the replication controller; populated by the system, read-only"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize replication controllers"` } @@ -581,8 +603,9 @@ type Service struct { // An external load balancer should be set up via the cloud-provider CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` - // PublicIPs are used by external load balancers. - PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs from which to select the address for the external load balancer"` + // PublicIPs are used by external load balancers, or can be set by + // users to handle external traffic that arrives at a node. + PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` // ContainerPort is the name or number of the port on the container to direct traffic to. // This is useful if the containers the service points to have multiple open ports. @@ -601,6 +624,12 @@ type Service struct { SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` } +// EndpointObjectReference is a reference to an object exposing the endpoint +type EndpointObjectReference struct { + Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"` + ObjectReference `json:"targetRef" description:"reference to the object providing the entry point"` +} + // Endpoints is a collection of endpoints that implement the actual service, for example: // Name: "mysql", Endpoints: ["10.10.1.1:1909", "10.10.2.2:8834"] type Endpoints struct { @@ -609,6 +638,8 @@ type Endpoints struct { // "UDP". Defaults to "TCP". Protocol Protocol `json:"protocol,omitempty" description:"IP protocol for endpoint ports; must be UDP or TCP; TCP if unspecified"` Endpoints []string `json:"endpoints" description:"list of endpoints corresponding to a service, of the form address:port, such as 10.10.1.1:1909"` + // Optional: The kubernetes object related to the entry point. + TargetRefs []EndpointObjectReference `json:"targetRefs,omitempty" description:"list of references to objects providing the endpoints"` } // EndpointsList is a list of endpoints. @@ -617,12 +648,33 @@ type EndpointsList struct { Items []Endpoints `json:"items" description:"list of service endpoint lists"` } +// NodeSystemInfo is a set of ids/uuids to uniquely identify the node. +type NodeSystemInfo struct { + // MachineID is the machine-id reported by the node + MachineID string `json:"machineID" description:"machine id is the machine-id reported by the node"` + // SystemUUID is the system-uuid reported by the node + SystemUUID string `json:"systemUUID" description:"system uuid is the system-uuid reported by the node"` +} + // NodeStatus is information about the current status of a node. type NodeStatus struct { // NodePhase is the current lifecycle phase of the node. Phase NodePhase `json:"phase,omitempty" description:"node phase is the current lifecycle phase of the node"` // Conditions is an array of current node conditions. Conditions []NodeCondition `json:"conditions,omitempty" description:"conditions is an array of current node conditions"` + // Queried from cloud provider, if available. + Addresses []NodeAddress `json:"addresses,omitempty" description:"list of addresses reachable to the node"` + // NodeSystemInfo is a set of ids/uuids to uniquely identify the node + NodeInfo NodeSystemInfo `json:"nodeInfo,omitempty" description:"node identity is a set of ids/uuids to uniquely identify the node"` +} + +// NodeInfo is the information collected on the node. +type NodeInfo struct { + TypeMeta `json:",inline"` + // Capacity represents the available resources. + Capacity ResourceList `json:"capacity,omitempty" description:"resource capacity of a node represented as a map of resource name to quantity of resource"` + // NodeSystemInfo is a set of ids/uuids to uniquely identify the node + NodeSystemInfo `json:",inline,omitempty" description:"node identity is a set of ids/uuids to uniquely identify the node"` } type NodePhase string @@ -647,6 +699,8 @@ const ( NodeReachable NodeConditionKind = "Reachable" // NodeReady means the node returns StatusOK for HTTP health check. NodeReady NodeConditionKind = "Ready" + // NodeSchedulable means the node is ready to accept new pods. + NodeSchedulable NodeConditionKind = "Schedulable" ) type NodeCondition struct { @@ -658,6 +712,20 @@ type NodeCondition struct { Message string `json:"message,omitempty" description:"human readable message indicating details about last transition"` } +type NodeAddressType string + +// These are valid address types of node. +const ( + NodeHostName NodeAddressType = "Hostname" + NodeExternalIP NodeAddressType = "ExternalIP" + NodeInternalIP NodeAddressType = "InternalIP" +) + +type NodeAddress struct { + Type NodeAddressType `json:"type" description:"type of the node address, e.g. external ip, internal ip, hostname, etc"` + Address string `json:"address" description:"string representation of the address"` +} + // NodeResources represents resources on a Kubernetes system node // see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md for more details. type NodeResources struct { @@ -680,16 +748,21 @@ type ResourceList map[ResourceName]util.IntOrString // The name of the minion according to etcd is in ID. type Minion struct { TypeMeta `json:",inline"` + // DEPRECATED: Use Status.Addresses instead. // Queried from cloud provider, if available. HostIP string `json:"hostIP,omitempty" description:"IP address of the node"` // Resources available on the node NodeResources NodeResources `json:"resources,omitempty" description:"characterization of node resources"` // Pod IP range assigned to the node - PodCIDR string `json:"cidr,omitempty" description:"IP range assigned to the node"` + PodCIDR string `json:"podCIDR,omitempty" description:"IP range assigned to the node"` + // Unschedulable controls node schedulability of new pods. By default node is schedulable. + Unschedulable bool `json:"unschedulable,omitempty" description:"disable pod scheduling on the node"` // Status describes the current status of a node Status NodeStatus `json:"status,omitempty" description:"current status of node"` // Labels for the node Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize minions; labels of a minion assigned by the scheduler must match the scheduled pod's nodeSelector"` + // External ID of the node + ExternalID string `json:"externalID,omitempty" description:"external id of the node assigned by some machine database (e.g. a cloud provider)"` } // MinionList is a list of minions. @@ -707,8 +780,20 @@ type NamespaceSpec struct { // NamespaceStatus is information about the current status of a Namespace. type NamespaceStatus struct { + // Phase is the current lifecycle phase of the namespace. + Phase NamespacePhase `json:"phase,omitempty" description:"phase is the current lifecycle phase of the namespace"` } +type NamespacePhase string + +// These are the valid phases of a namespace. +const ( + // NamespaceActive means the namespace is available for use in the system + NamespaceActive NamespacePhase = "Active" + // NamespaceTerminating means the namespace is undergoing graceful termination + NamespaceTerminating NamespacePhase = "Terminating" +) + // A namespace provides a scope for Names. // Use of multiple namespaces is optional type Namespace struct { @@ -721,7 +806,7 @@ type Namespace struct { Spec NamespaceSpec `json:"spec,omitempty" description:"spec defines the behavior of the Namespace"` // Status describes the current status of a Namespace - Status NamespaceStatus `json:"status,omitempty" description:"status describes the current status of a Namespace"` + Status NamespaceStatus `json:"status,omitempty" description:"status describes the current status of a Namespace; read-only"` } // NamespaceList is a list of Namespaces. @@ -739,6 +824,16 @@ type Binding struct { Host string `json:"host" description:"host to which to bind the specified pod"` } +// DeleteOptions may be provided when deleting an API object +type DeleteOptions struct { + TypeMeta `json:",inline"` + + // Optional duration in seconds before the object should be deleted. Value must be non-negative integer. + // The value zero indicates delete immediately. If this value is nil, the default grace period for the + // specified type will be used. + GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` +} + // Status is a return value for calls that don't return other objects. // TODO: this could go in apiserver, but I'm including it here so clients needn't // import both. @@ -885,7 +980,7 @@ type ObjectReference struct { ID string `json:"name,omitempty" description:"id of the referent"` UID types.UID `json:"uid,omitempty" description:"uid of the referent"` APIVersion string `json:"apiVersion,omitempty" description:"API version of the referent"` - ResourceVersion string `json:"resourceVersion,omitempty" description:"specific resourceVersion to which this reference is made, if any"` + ResourceVersion string `json:"resourceVersion,omitempty" description:"specific resourceVersion to which this reference is made, if any: https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#concurrency-control-and-consistency"` // Optional. If referring to a piece of an object instead of an entire object, this string // should contain information to identify the sub-object. For example, if the object @@ -971,8 +1066,9 @@ const ( // PodSpec is a description of a pod type PodSpec struct { - Volumes []Volume `json:"volumes" description:"list of volumes that can be mounted by containers belonging to the pod"` - Containers []Container `json:"containers" description:"list of containers belonging to the pod"` + Volumes []Volume `json:"volumes" description:"list of volumes that can be mounted by containers belonging to the pod"` + // Required: there must be at least one container in a pod. + Containers []Container `json:"containers" description:"list of containers belonging to the pod; containers cannot currently be added or removed; there must be at least one container in a Pod"` RestartPolicy RestartPolicy `json:"restartPolicy,omitempty" description:"restart policy for all containers within the pod; one of RestartPolicyAlways, RestartPolicyOnFailure, RestartPolicyNever"` // Optional: Set DNS policy. Defaults to "ClusterFirst" DNSPolicy DNSPolicy `json:"dnsPolicy,omitempty" description:"DNS policy for containers within the pod; one of 'ClusterFirst' or 'Default'"` @@ -985,28 +1081,6 @@ type PodSpec struct { Host string `json:"host,omitempty" description:"host requested for this pod"` } -// BoundPod is a collection of containers that should be run on a host. A BoundPod -// defines how a Pod may change after a Binding is created. A Pod is a request to -// execute a pod, whereas a BoundPod is the specification that would be run on a server. -type BoundPod struct { - TypeMeta `json:",inline"` - - // Spec defines the behavior of a pod. - Spec PodSpec `json:"spec,omitempty" description:"specification of the desired state of containers and volumes comprising the pod"` -} - -// BoundPods is a list of Pods bound to a common server. The resource version of -// the pod list is guaranteed to only change when the list of bound pods changes. -type BoundPods struct { - TypeMeta `json:",inline"` - - // Host is the name of a node that these pods were bound to. - Host string `json:"host" description:"name of a node that these pods were bound to"` - - // Items is the list of all pods bound to a given host. - Items []BoundPod `json:"items" description:"list of all pods bound to a given host"` -} - // List holds a list of objects, which may not be known by the server. type List struct { TypeMeta `json:",inline"` @@ -1084,6 +1158,7 @@ type ResourceQuotaStatus struct { // ResourceQuota sets aggregate quota restrictions enforced per namespace type ResourceQuota struct { TypeMeta `json:",inline"` + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize resource quotas"` // Spec defines the desired quota Spec ResourceQuotaSpec `json:"spec,omitempty" description:"spec defines the desired quota"` @@ -1092,15 +1167,6 @@ type ResourceQuota struct { Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` } -// ResourceQuotaUsage captures system observed quota status per namespace -// It is used to enforce atomic updates of a backing ResourceQuota.Status field in storage -type ResourceQuotaUsage struct { - TypeMeta `json:",inline"` - - // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` -} - // ResourceQuotaList is a list of ResourceQuota items type ResourceQuotaList struct { TypeMeta `json:",inline"` @@ -1109,6 +1175,19 @@ type ResourceQuotaList struct { Items []ResourceQuota `json:"items" description:"items is a list of ResourceQuota objects"` } +// NFSVolumeSource represents an NFS Mount that lasts the lifetime of a pod +type NFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `json:"server" description:"the hostname or IP address of the NFS server"` + + // Path is the exported NFS share + Path string `json:"path" description:"the path that is exported by the NFS server"` + + // Optional: Defaults to false (read/write). ReadOnly here will force + // the NFS export to be mounted as read-only permissions + ReadOnly bool `json:"readOnly,omitempty" description:"forces the NFS export to be mounted with read-only permissions"` +} + // Secret holds secret data of a certain type. The total bytes of the values in // the Data field must be less than MaxSecretSize bytes. type Secret struct { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/conversion.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/conversion.go index 837b9366a90c..c25e13ffd52a 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/conversion.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/conversion.go @@ -36,6 +36,7 @@ func init() { newer.Scheme.AddStructFieldConversion(newer.TypeMeta{}, "TypeMeta", TypeMeta{}, "TypeMeta") newer.Scheme.AddStructFieldConversion(newer.ObjectMeta{}, "ObjectMeta", TypeMeta{}, "TypeMeta") newer.Scheme.AddStructFieldConversion(newer.ListMeta{}, "ListMeta", TypeMeta{}, "TypeMeta") + newer.Scheme.AddStructFieldConversion(newer.Endpoints{}, "Endpoints", Endpoints{}, "Endpoints") // TODO: scope this to a specific type once that becomes available and remove the Event conversion functions below // newer.Scheme.AddStructFieldConversion(string(""), "Status", string(""), "Condition") @@ -83,6 +84,7 @@ func init() { out.GenerateName = in.GenerateName out.UID = in.UID out.CreationTimestamp = in.CreationTimestamp + out.DeletionTimestamp = in.DeletionTimestamp out.SelfLink = in.SelfLink if len(in.ResourceVersion) > 0 { v, err := strconv.ParseUint(in.ResourceVersion, 10, 64) @@ -99,6 +101,7 @@ func init() { out.GenerateName = in.GenerateName out.UID = in.UID out.CreationTimestamp = in.CreationTimestamp + out.DeletionTimestamp = in.DeletionTimestamp out.SelfLink = in.SelfLink if in.ResourceVersion != 0 { out.ResourceVersion = strconv.FormatUint(in.ResourceVersion, 10) @@ -616,9 +619,21 @@ func init() { if err := s.Convert(&in.Status.Conditions, &out.Status.Conditions, 0); err != nil { return err } + if err := s.Convert(&in.Status.Addresses, &out.Status.Addresses, 0); err != nil { + return err + } + if err := s.Convert(&in.Status.NodeInfo, &out.Status.NodeInfo, 0); err != nil { + return err + } - out.HostIP = in.Status.HostIP + for _, address := range in.Status.Addresses { + if address.Type == newer.NodeLegacyHostIP { + out.HostIP = address.Address + } + } out.PodCIDR = in.Spec.PodCIDR + out.ExternalID = in.Spec.ExternalID + out.Unschedulable = in.Spec.Unschedulable return s.Convert(&in.Spec.Capacity, &out.NodeResources.Capacity, 0) }, func(in *Minion, out *newer.Node, s conversion.Scope) error { @@ -637,11 +652,23 @@ func init() { if err := s.Convert(&in.Status.Conditions, &out.Status.Conditions, 0); err != nil { return err } + if err := s.Convert(&in.Status.Addresses, &out.Status.Addresses, 0); err != nil { + return err + } + if err := s.Convert(&in.Status.NodeInfo, &out.Status.NodeInfo, 0); err != nil { + return err + } - out.Status.HostIP = in.HostIP + if in.HostIP != "" { + newer.AddToNodeAddresses(&out.Status.Addresses, + newer.NodeAddress{Type: newer.NodeLegacyHostIP, Address: in.HostIP}) + } out.Spec.PodCIDR = in.PodCIDR + out.Spec.ExternalID = in.ExternalID + out.Spec.Unschedulable = in.Unschedulable return s.Convert(&in.NodeResources.Capacity, &out.Spec.Capacity, 0) }, + func(in *newer.LimitRange, out *LimitRange, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -666,6 +693,7 @@ func init() { } return nil }, + func(in *Namespace, out *newer.Namespace, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -676,11 +704,15 @@ func init() { if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { return err } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } if err := s.Convert(&in.Labels, &out.ObjectMeta.Labels, 0); err != nil { return err } return nil }, + func(in *newer.LimitRangeSpec, out *LimitRangeSpec, s conversion.Scope) error { *out = LimitRangeSpec{} out.Limits = make([]LimitRangeItem, len(in.Limits), len(in.Limits)) @@ -701,6 +733,7 @@ func init() { } return nil }, + func(in *newer.LimitRangeItem, out *LimitRangeItem, s conversion.Scope) error { *out = LimitRangeItem{} out.Type = LimitType(in.Type) @@ -723,6 +756,7 @@ func init() { } return nil }, + func(in *newer.ResourceQuota, out *ResourceQuota, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -736,6 +770,9 @@ func init() { if err := s.Convert(&in.Status, &out.Status, 0); err != nil { return err } + if err := s.Convert(&in.Labels, &out.Labels, 0); err != nil { + return err + } return nil }, func(in *ResourceQuota, out *newer.ResourceQuota, s conversion.Scope) error { @@ -751,32 +788,12 @@ func init() { if err := s.Convert(&in.Status, &out.Status, 0); err != nil { return err } - return nil - }, - func(in *newer.ResourceQuotaUsage, out *ResourceQuotaUsage, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { - return err - } - return nil - }, - func(in *ResourceQuotaUsage, out *newer.ResourceQuotaUsage, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.TypeMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + if err := s.Convert(&in.Labels, &out.ObjectMeta.Labels, 0); err != nil { return err } return nil }, + func(in *newer.ResourceQuotaSpec, out *ResourceQuotaSpec, s conversion.Scope) error { *out = ResourceQuotaSpec{} if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { @@ -791,6 +808,7 @@ func init() { } return nil }, + func(in *newer.ResourceQuotaStatus, out *ResourceQuotaStatus, s conversion.Scope) error { *out = ResourceQuotaStatus{} if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { @@ -811,6 +829,7 @@ func init() { } return nil }, + // Object ID <-> Name // TODO: amend the conversion package to allow overriding specific fields. func(in *ObjectReference, out *newer.ObjectReference, s conversion.Scope) error { @@ -933,6 +952,22 @@ func init() { } return nil }, + + func(in *newer.Volume, out *Volume, s conversion.Scope) error { + if err := s.Convert(&in.VolumeSource, &out.Source, 0); err != nil { + return err + } + out.Name = in.Name + return nil + }, + func(in *Volume, out *newer.Volume, s conversion.Scope) error { + if err := s.Convert(&in.Source, &out.VolumeSource, 0); err != nil { + return err + } + out.Name = in.Name + return nil + }, + func(in *newer.VolumeSource, out *VolumeSource, s conversion.Scope) error { if err := s.Convert(&in.EmptyDir, &out.EmptyDir, 0); err != nil { return err @@ -949,6 +984,9 @@ func init() { if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil { return err } + if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { + return err + } return nil }, func(in *VolumeSource, out *newer.VolumeSource, s conversion.Scope) error { @@ -967,6 +1005,9 @@ func init() { if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil { return err } + if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { + return err + } return nil }, @@ -1003,6 +1044,33 @@ func init() { return nil }, + func(in *newer.RestartPolicy, out *RestartPolicy, s conversion.Scope) error { + switch *in { + case newer.RestartPolicyAlways: + *out = RestartPolicy{Always: &RestartPolicyAlways{}} + case newer.RestartPolicyNever: + *out = RestartPolicy{Never: &RestartPolicyNever{}} + case newer.RestartPolicyOnFailure: + *out = RestartPolicy{OnFailure: &RestartPolicyOnFailure{}} + default: + *out = RestartPolicy{} + } + return nil + }, + func(in *RestartPolicy, out *newer.RestartPolicy, s conversion.Scope) error { + switch { + case in.Always != nil: + *out = newer.RestartPolicyAlways + case in.Never != nil: + *out = newer.RestartPolicyNever + case in.OnFailure != nil: + *out = newer.RestartPolicyOnFailure + default: + *out = "" + } + return nil + }, + func(in *newer.Probe, out *LivenessProbe, s conversion.Scope) error { if err := s.Convert(&in.Exec, &out.Exec, 0); err != nil { return err @@ -1031,6 +1099,7 @@ func init() { out.TimeoutSeconds = in.TimeoutSeconds return nil }, + func(in *newer.Endpoints, out *Endpoints, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -1043,7 +1112,17 @@ func init() { } for i := range in.Endpoints { ep := &in.Endpoints[i] - out.Endpoints = append(out.Endpoints, net.JoinHostPort(ep.IP, strconv.Itoa(ep.Port))) + hostPort := net.JoinHostPort(ep.IP, strconv.Itoa(ep.Port)) + out.Endpoints = append(out.Endpoints, hostPort) + if ep.TargetRef != nil { + target := EndpointObjectReference{ + Endpoint: hostPort, + } + if err := s.Convert(ep.TargetRef, &target.ObjectReference, 0); err != nil { + return err + } + out.TargetRefs = append(out.TargetRefs, target) + } } return nil }, @@ -1070,7 +1149,160 @@ func init() { return err } ep.Port = pn + for j := range in.TargetRefs { + if in.TargetRefs[j].Endpoint != in.Endpoints[i] { + continue + } + ep.TargetRef = &newer.ObjectReference{} + if err := s.Convert(&in.TargetRefs[j], ep.TargetRef, 0); err != nil { + return err + } + } + } + return nil + }, + + func(in *newer.NodeCondition, out *NodeCondition, s conversion.Scope) error { + if err := s.Convert(&in.Type, &out.Kind, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + if err := s.Convert(&in.LastProbeTime, &out.LastProbeTime, 0); err != nil { + return err + } + if err := s.Convert(&in.LastTransitionTime, &out.LastTransitionTime, 0); err != nil { + return err } + if err := s.Convert(&in.Reason, &out.Reason, 0); err != nil { + return err + } + if err := s.Convert(&in.Message, &out.Message, 0); err != nil { + return err + } + return nil + }, + func(in *NodeCondition, out *newer.NodeCondition, s conversion.Scope) error { + if err := s.Convert(&in.Kind, &out.Type, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + if err := s.Convert(&in.LastProbeTime, &out.LastProbeTime, 0); err != nil { + return err + } + if err := s.Convert(&in.LastTransitionTime, &out.LastTransitionTime, 0); err != nil { + return err + } + if err := s.Convert(&in.Reason, &out.Reason, 0); err != nil { + return err + } + if err := s.Convert(&in.Message, &out.Message, 0); err != nil { + return err + } + return nil + }, + + func(in *newer.NodeConditionType, out *NodeConditionKind, s conversion.Scope) error { + switch *in { + case newer.NodeReachable: + *out = NodeReachable + break + case newer.NodeReady: + *out = NodeReady + break + case "": + *out = "" + default: + *out = NodeConditionKind(*in) + break + } + + return nil + }, + func(in *NodeConditionKind, out *newer.NodeConditionType, s conversion.Scope) error { + switch *in { + case NodeReachable: + *out = newer.NodeReachable + break + case NodeReady: + *out = newer.NodeReady + break + case "": + *out = "" + default: + *out = newer.NodeConditionType(*in) + break + } + + return nil + }, + + func(in *newer.PodCondition, out *PodCondition, s conversion.Scope) error { + if err := s.Convert(&in.Type, &out.Kind, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil + }, + func(in *PodCondition, out *newer.PodCondition, s conversion.Scope) error { + if err := s.Convert(&in.Kind, &out.Type, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil + }, + + func(in *newer.PodConditionType, out *PodConditionKind, s conversion.Scope) error { + switch *in { + case newer.PodReady: + *out = PodReady + break + case "": + *out = "" + default: + *out = PodConditionKind(*in) + break + } + + return nil + }, + func(in *PodConditionKind, out *newer.PodConditionType, s conversion.Scope) error { + switch *in { + case PodReady: + *out = newer.PodReady + break + case "": + *out = "" + default: + *out = newer.PodConditionType(*in) + break + } + + return nil + }, + func(in *Binding, out *newer.Binding, s conversion.Scope) error { + if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil { + return err + } + out.Target = newer.ObjectReference{ + Name: in.Host, + } + out.Name = in.PodID + return nil + }, + func(in *newer.Binding, out *Binding, s conversion.Scope) error { + if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil { + return err + } + out.Host = in.Target.Name + out.PodID = in.Name return nil }, ) @@ -1078,4 +1310,48 @@ func init() { // If one of the conversion functions is malformed, detect it immediately. panic(err) } + + // Add field conversion funcs. + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "pods", + func(label, value string) (string, string, error) { + switch label { + case "name": + return "name", value, nil + case "DesiredState.Host": + return "spec.host", value, nil + case "DesiredState.Status": + podStatus := PodStatus(value) + var internalValue newer.PodPhase + newer.Scheme.Convert(&podStatus, &internalValue) + return "status.phase", string(internalValue), nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "events", + func(label, value string) (string, string, error) { + switch label { + case "involvedObject.kind", + "involvedObject.namespace", + "involvedObject.uid", + "involvedObject.apiVersion", + "involvedObject.resourceVersion", + "involvedObject.fieldPath", + "reason", + "source": + return label, value, nil + case "involvedObject.id": + return "involvedObject.name", value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/defaults.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/defaults.go index db59b8cd272e..9c5cf60163ad 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/defaults.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/defaults.go @@ -34,7 +34,7 @@ func init() { } } }, - func(obj *Port) { + func(obj *ContainerPort) { if obj.Protocol == "" { obj.Protocol = ProtocolTCP } @@ -92,5 +92,15 @@ func init() { obj.Protocol = "TCP" } }, + func(obj *HTTPGetAction) { + if obj.Path == "" { + obj.Path = "/" + } + }, + func(obj *NamespaceStatus) { + if obj.Phase == "" { + obj.Phase = NamespaceActive + } + }, ) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/defaults_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/defaults_test.go index a73c83a93e63..0649462c4e94 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/defaults_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/defaults_test.go @@ -51,48 +51,6 @@ func TestSetDefaultService(t *testing.T) { } } -func TestSetDefaulPodSpec(t *testing.T) { - bp := ¤t.BoundPod{} - bp.Spec.Volumes = []current.Volume{{}} - - obj2 := roundTrip(t, runtime.Object(bp)) - bp2 := obj2.(*current.BoundPod) - if bp2.Spec.DNSPolicy != current.DNSClusterFirst { - t.Errorf("Expected default dns policy :%s, got: %s", current.DNSClusterFirst, bp2.Spec.DNSPolicy) - } - policy := bp2.Spec.RestartPolicy - if policy.Never != nil || policy.OnFailure != nil || policy.Always == nil { - t.Errorf("Expected only policy.Always is set, got: %s", policy) - } - vsource := bp2.Spec.Volumes[0].Source - if vsource.EmptyDir == nil { - t.Errorf("Expected non-empty volume is set, got: %s", vsource.EmptyDir) - } -} - -func TestSetDefaultContainer(t *testing.T) { - bp := ¤t.BoundPod{} - bp.Spec.Containers = []current.Container{{}} - bp.Spec.Containers[0].Ports = []current.Port{{}} - - obj2 := roundTrip(t, runtime.Object(bp)) - bp2 := obj2.(*current.BoundPod) - - container := bp2.Spec.Containers[0] - if container.TerminationMessagePath != current.TerminationMessagePathDefault { - t.Errorf("Expected termination message path: %s, got: %s", - current.TerminationMessagePathDefault, container.TerminationMessagePath) - } - if container.ImagePullPolicy != current.PullIfNotPresent { - t.Errorf("Expected image pull policy: %s, got: %s", - current.PullIfNotPresent, container.ImagePullPolicy) - } - if container.Ports[0].Protocol != current.ProtocolTCP { - t.Errorf("Expected protocol: %s, got: %s", - current.ProtocolTCP, container.Ports[0].Protocol) - } -} - func TestSetDefaultSecret(t *testing.T) { s := ¤t.Secret{} obj2 := roundTrip(t, runtime.Object(s)) @@ -112,3 +70,13 @@ func TestSetDefaulEndpointsProtocol(t *testing.T) { t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Protocol) } } + +func TestSetDefaultNamespace(t *testing.T) { + s := ¤t.Namespace{} + obj2 := roundTrip(t, runtime.Object(s)) + s2 := obj2.(*current.Namespace) + + if s2.Status.Phase != current.NamespaceActive { + t.Errorf("Expected phase %v, got %v", current.NamespaceActive, s2.Status.Phase) + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/register.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/register.go index 990aa7b20397..caf553af06a5 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/register.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/register.go @@ -24,6 +24,12 @@ import ( // Codec encodes internal objects to the v1beta2 scheme var Codec = runtime.CodecFor(api.Scheme, "v1beta2") +// Dependency does nothing but give a hook for other packages to force a +// compile-time error when this API version is eventually removed. This is +// useful, for example, to clean up things that are implicitly tied to +// semantics of older APIs. +const Dependency = true + func init() { api.Scheme.AddKnownTypes("v1beta2", &Pod{}, @@ -36,6 +42,7 @@ func init() { &Endpoints{}, &EndpointsList{}, &Minion{}, + &NodeInfo{}, &MinionList{}, &Binding{}, &Status{}, @@ -43,18 +50,16 @@ func init() { &EventList{}, &ContainerManifest{}, &ContainerManifestList{}, - &BoundPod{}, - &BoundPods{}, &List{}, &LimitRange{}, &LimitRangeList{}, &ResourceQuota{}, &ResourceQuotaList{}, - &ResourceQuotaUsage{}, &Namespace{}, &NamespaceList{}, &Secret{}, &SecretList{}, + &DeleteOptions{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) @@ -71,6 +76,7 @@ func (*ServiceList) IsAnAPIObject() {} func (*Endpoints) IsAnAPIObject() {} func (*EndpointsList) IsAnAPIObject() {} func (*Minion) IsAnAPIObject() {} +func (*NodeInfo) IsAnAPIObject() {} func (*MinionList) IsAnAPIObject() {} func (*Binding) IsAnAPIObject() {} func (*Status) IsAnAPIObject() {} @@ -78,15 +84,13 @@ func (*Event) IsAnAPIObject() {} func (*EventList) IsAnAPIObject() {} func (*ContainerManifest) IsAnAPIObject() {} func (*ContainerManifestList) IsAnAPIObject() {} -func (*BoundPod) IsAnAPIObject() {} -func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} -func (*ResourceQuotaUsage) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/types.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/types.go index 6787cc6c32e9..d5adb6d33dac 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/types.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2/types.go @@ -46,6 +46,8 @@ import ( // DNS_LABEL(\.DNS_LABEL)* // Volume represents a named volume in a pod that may be accessed by any containers in the pod. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/volumes.md type Volume struct { // Required: This must be a DNS_LABEL. Each volume in a pod must have // a unique name. @@ -58,6 +60,8 @@ type Volume struct { // VolumeSource represents the source location of a valume to mount. // Only one of its members may be specified. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/volumes.md#types-of-volumes type VolumeSource struct { // HostDir represents a pre-existing directory on the host machine that is directly // exposed to the container. This is generally used for system agents or other privileged @@ -74,16 +78,37 @@ type VolumeSource struct { GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` // Secret is a secret to populate the volume with Secret *SecretVolumeSource `json:"secret" description:"secret to populate volume"` + // NFS represents an NFS mount on the host that shares a pod's lifetime + NFS *NFSVolumeSource `json:"nfs" description:"NFS volume that will be mounted in the host machine"` } // HostPathVolumeSource represents bare host directory volume. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/volumes.md#hostdir type HostPathVolumeSource struct { Path string `json:"path" description:"path of the directory on the host"` } -type EmptyDirVolumeSource struct{} +// Represents an empty directory volume. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/volumes.md#emptydir +type EmptyDirVolumeSource struct { + // Optional: what type of storage medium should back this directory. + // The default is "" which means to use the node's default medium. + Medium StorageType `json:"medium" description:"type of storage used to back the volume; must be an empty string (default) or Memory"` +} + +// StorageType defines ways that storage can be allocated to a volume. +type StorageType string + +const ( + StorageTypeDefault StorageType = "" // use whatever the default is for the node + StorageTypeMemory StorageType = "Memory" // use memory (tmpfs) +) // SecretVolumeSource adapts a Secret into a VolumeSource +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/design/secrets.md type SecretVolumeSource struct { // Reference to a Secret Target ObjectReference `json:"target" description:"target is a reference to a secret"` @@ -99,8 +124,8 @@ const ( ProtocolUDP Protocol = "UDP" ) -// Port represents a network port in a single container. -type Port struct { +// ContainerPort represents a network port in a single container. +type ContainerPort struct { // Optional: If specified, this must be a DNS_LABEL. Each named port // in a pod must have a unique name. Name string `json:"name,omitempty" description:"name for the port that can be referred to by services; must be a DNS_LABEL and unique without the pod"` @@ -119,6 +144,8 @@ type Port struct { // A GCE PD must exist and be formatted before mounting to a container. // The disk must also be in the same GCE project and zone as the kubelet. // A GCE PD can only be mounted as read/write once. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/volumes.md#gcepersistentdisk type GCEPersistentDiskVolumeSource struct { // Unique name of the PD resource. Used to identify the disk in GCE PDName string `json:"pdName" description:"unique name of the PD resource in GCE"` @@ -146,6 +173,8 @@ type GitRepoVolumeSource struct { } // VolumeMount describes a mounting of a Volume within a container. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/volumes.md type VolumeMount struct { // Required: This must match the Name of a Volume [above]. Name string `json:"name" description:"name of the volume to mount"` @@ -164,6 +193,8 @@ type EnvVar struct { } // HTTPGetAction describes an action based on HTTP Get requests. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/container-environment.md#hook-handler-implementations type HTTPGetAction struct { // Optional: Path to access on the HTTP server. Path string `json:"path,omitempty" description:"path to access on the HTTP server"` @@ -174,12 +205,16 @@ type HTTPGetAction struct { } // TCPSocketAction describes an action based on opening a socket +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/container-environment.md#hook-handler-implementations type TCPSocketAction struct { // Required: Port to connect to. Port util.IntOrString `json:"port,omitempty" description:"number of name of the port to access on the container"` } // ExecAction describes a "run in container" action. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/container-environment.md#hook-handler-implementations type ExecAction struct { // Command is the command line to execute inside the container, the working directory for the // command is root ('/') in the container's filesystem. The command is simply exec'd, it is @@ -204,6 +239,8 @@ type LivenessProbe struct { } // PullPolicy describes a policy for if/when to pull a container image +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/images.md#preloading-images type PullPolicy string const ( @@ -219,6 +256,8 @@ const ( type CapabilityType string // Capabilities represent POSIX capabilities that can be added or removed to a running container. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/containers.md#capabilities type Capabilities struct { // Added capabilities Add []CapabilityType `json:"add,omitempty" description:"added capabilities"` @@ -232,35 +271,37 @@ type ResourceRequirements struct { } // Container represents a single container that is expected to be run on the host. +// +// type Container struct { // Required: This must be a DNS_LABEL. Each container in a pod must // have a unique name. - Name string `json:"name" description:"name of the container; must be a DNS_LABEL and unique within the pod"` + Name string `json:"name" description:"name of the container; must be a DNS_LABEL and unique within the pod; cannot be updated"` // Required. Image string `json:"image" description:"Docker image name"` // Optional: Defaults to whatever is defined in the image. - Command []string `json:"command,omitempty" description:"command argv array; not executed within a shell; defaults to entrypoint or command in the image"` + Command []string `json:"command,omitempty" description:"command argv array; not executed within a shell; defaults to entrypoint or command in the image; cannot be updated"` // Optional: Defaults to Docker's default. - WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default"` - Ports []Port `json:"ports,omitempty" description:"list of ports to expose from the container"` - Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container"` - Resources ResourceRequirements `json:"resources,omitempty" description:"Compute Resources required by this container"` + WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default; cannot be updated"` + Ports []ContainerPort `json:"ports,omitempty" description:"list of ports to expose from the container; cannot be updated"` + Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container; cannot be updated"` + Resources ResourceRequirements `json:"resources,omitempty" description:"Compute Resources required by this container; cannot be updated"` // Optional: Defaults to unlimited. - CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core"` + CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core; cannot be updated"` // Optional: Defaults to unlimited. - Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` - VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesystem"` - LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty" description:"periodic probe of container liveness; container will be restarted if the probe fails"` - ReadinessProbe *LivenessProbe `json:"readinessProbe,omitempty" description:"periodic probe of container service readiness; container will be removed from service endpoints if the probe fails"` - Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events"` + Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited; cannot be updated"` + VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesystem; cannot be updated"` + LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty" description:"periodic probe of container liveness; container will be restarted if the probe fails; cannot be updated"` + ReadinessProbe *LivenessProbe `json:"readinessProbe,omitempty" description:"periodic probe of container service readiness; container will be removed from service endpoints if the probe fails; cannot be updated"` + Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log - TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log"` + TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false"` + Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` // Optional: Policy for pulling images for this container - ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise"` + ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container"` + Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` } const ( @@ -270,6 +311,8 @@ const ( // Handler defines a specific action that should be taken // TODO: pass structured data to these actions, and document that data here. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/container-environment.md#hook-handler-implementations type Handler struct { // One and only one of the following should be specified. // Exec specifies the action to take. @@ -284,6 +327,8 @@ type Handler struct { // Lifecycle describes actions that the management system should take in response to container lifecycle // events. For the PostStart and PreStop lifecycle handlers, management of the container blocks // until the action is complete, unless the container process fails, in which case the handler is aborted. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/container-environment.md#hook-details type Lifecycle struct { // PostStart is called immediately after a container is created. If the handler fails, the container // is terminated and restarted. @@ -297,14 +342,25 @@ type Lifecycle struct { // TypeMeta is shared by all objects sent to, or returned from the client. type TypeMeta struct { - Kind string `json:"kind,omitempty" description:"kind of object, in CamelCase"` - ID string `json:"id,omitempty" description:"name of the object; must be a DNS_SUBDOMAIN and unique among all objects of the same kind within the same namespace; used in resource URLs"` - UID types.UID `json:"uid,omitempty" description:"UUID assigned by the system upon creation, unique across space and time"` - CreationTimestamp util.Time `json:"creationTimestamp,omitempty" description:"RFC 3339 date and time at which the object was created; recorded by the system; null for lists"` - SelfLink string `json:"selfLink,omitempty" description:"URL for the object"` - ResourceVersion uint64 `json:"resourceVersion,omitempty" description:"string that identifies the internal version of this object that can be used by clients to determine when objects have changed; value must be treated as opaque by clients and passed unmodified back to the server"` + Kind string `json:"kind,omitempty" description:"kind of object, in CamelCase; cannot be updated"` + ID string `json:"id,omitempty" description:"name of the object; must be a DNS_SUBDOMAIN and unique among all objects of the same kind within the same namespace; used in resource URLs; cannot be updated"` + UID types.UID `json:"uid,omitempty" description:"unique UUID across space and time; populated by the system, read-only"` + CreationTimestamp util.Time `json:"creationTimestamp,omitempty" description:"RFC 3339 date and time at which the object was created; populated by the system, read-only; null for lists"` + SelfLink string `json:"selfLink,omitempty" description:"URL for the object; populated by the system, read-only"` + ResourceVersion uint64 `json:"resourceVersion,omitempty" description:"string that identifies the internal version of this object that can be used by clients to determine when objects have changed; populated by the system, read-only; value must be treated as opaque by clients and passed unmodified back to the server: https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#concurrency-control-and-consistency"` APIVersion string `json:"apiVersion,omitempty" description:"version of the schema the object should have"` - Namespace string `json:"namespace,omitempty" description:"namespace to which the object belongs; must be a DNS_SUBDOMAIN; 'default' by default"` + Namespace string `json:"namespace,omitempty" description:"namespace to which the object belongs; must be a DNS_SUBDOMAIN; 'default' by default; cannot be updated"` + + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *util.Time `json:"deletionTimestamp,omitempty" description:"RFC 3339 date and time at which the object will be deleted; populated by the system when a graceful deletion is requested, read-only; if not set, graceful deletion of the object has not been requested"` // GenerateName indicates that the name should be made unique by the server prior to persisting // it. A non-empty value for the field indicates the name will be made unique (and the name @@ -338,6 +394,8 @@ const ( ) // PodStatus represents a status of a pod. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/pod-states.md type PodStatus string // These are the valid statuses of pods. @@ -389,9 +447,6 @@ type ContainerStatus struct { // Note that this is calculated from dead containers. But those containers are subject to // garbage collection. This value will get capped at 5 by GC. RestartCount int `json:"restartCount" description:"the number of times the container has been restarted, currently based on the number of dead containers that have not yet been removed"` - // TODO(dchen1107): Deprecated this soon once we pull entire PodStatus from node, - // not just PodInfo. Now we need this to remove docker.Container from API - PodIP string `json:"podIP,omitempty" description:"pod's IP address"` // TODO(dchen1107): Need to decide how to reprensent this in v1beta3 Image string `json:"image" description:"image of the container"` ImageID string `json:"imageID" description:"ID of the container's image"` @@ -449,7 +504,7 @@ type PodState struct { Conditions []PodCondition `json:"Condition,omitempty" description:"current service state of pod"` // A human readable message indicating details about why the pod is in this state. Message string `json:"message,omitempty" description:"human readable message indicating details about why the pod is in this condition"` - Host string `json:"host,omitempty" description:"host to which the pod is assigned; empty if not yet scheduled"` + Host string `json:"host,omitempty" description:"host to which the pod is assigned; empty if not yet scheduled; cannot be updated"` HostIP string `json:"hostIP,omitempty" description:"IP address of the host to which the pod is assigned; empty if not yet scheduled"` PodIP string `json:"podIP,omitempty" description:"IP address allocated to the pod; routable at least within the cluster; empty if not yet allocated"` @@ -471,11 +526,13 @@ type PodList struct { } // Pod is a collection of containers, used as either input (create, update) or as output (list, get). +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/pods.md type Pod struct { TypeMeta `json:",inline"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"` DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"` - CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod"` + CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"` // NodeSelector is a selector which must be true for the pod to fit on a node NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` } @@ -494,14 +551,18 @@ type ReplicationControllerList struct { } // ReplicationController represents the configuration of a replication controller. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/replication-controller.md type ReplicationController struct { TypeMeta `json:",inline"` DesiredState ReplicationControllerState `json:"desiredState,omitempty" description:"specification of the desired state of the replication controller"` - CurrentState ReplicationControllerState `json:"currentState,omitempty" description:"current state of the replication controller"` + CurrentState ReplicationControllerState `json:"currentState,omitempty" description:"current state of the replication controller; populated by the system, read-only"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize replication controllers"` } // PodTemplate holds the information used for creating pods. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/replication-controller.md#pod-template type PodTemplate struct { DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"` NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"` @@ -529,6 +590,8 @@ type ServiceList struct { // Service is a named abstraction of software service (for example, mysql) consisting of local port // (for example 3306) that the proxy listens on, and the selector that determines which pods // will answer requests sent through the proxy. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/services.md type Service struct { TypeMeta `json:",inline"` @@ -545,8 +608,9 @@ type Service struct { // An external load balancer should be set up via the cloud-provider CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` - // PublicIPs are used by external load balancers. - PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs from which to select the address for the external load balancer"` + // PublicIPs are used by external load balancers, or can be set by + // users to handle external traffic that arrives at a node. + PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` // ContainerPort is the name or number of the port on the container to direct traffic to. // This is useful if the containers the service points to have multiple open ports. @@ -565,6 +629,12 @@ type Service struct { SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` } +// EndpointObjectReference is a reference to an object exposing the endpoint +type EndpointObjectReference struct { + Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"` + ObjectReference `json:"targetRef" description:"reference to the object providing the entry point"` +} + // Endpoints is a collection of endpoints that implement the actual service, for example: // Name: "mysql", Endpoints: ["10.10.1.1:1909", "10.10.2.2:8834"] type Endpoints struct { @@ -573,6 +643,8 @@ type Endpoints struct { // "UDP". Defaults to "TCP". Protocol Protocol `json:"protocol,omitempty" description:"IP protocol for endpoint ports; must be UDP or TCP; TCP if unspecified"` Endpoints []string `json:"endpoints" description:"list of endpoints corresponding to a service, of the form address:port, such as 10.10.1.1:1909"` + // Optional: The kubernetes object related to the entry point. + TargetRefs []EndpointObjectReference `json:"targetRefs,omitempty" description:"list of references to objects providing the endpoints"` } // EndpointsList is a list of endpoints. @@ -581,14 +653,40 @@ type EndpointsList struct { Items []Endpoints `json:"items" description:"list of service endpoint lists"` } +// NodeSystemInfo is a set of ids/uuids to uniquely identify the node. +type NodeSystemInfo struct { + // MachineID is the machine-id reported by the node + MachineID string `json:"machineID" description:"machine id is the machine-id reported by the node"` + // SystemUUID is the system-uuid reported by the node + SystemUUID string `json:"systemUUID" description:"system uuid is the system-uuid reported by the node"` +} + // NodeStatus is information about the current status of a node. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/node.md#node-status type NodeStatus struct { // NodePhase is the current lifecycle phase of the node. Phase NodePhase `json:"phase,omitempty" description:"node phase is the current lifecycle phase of the node"` // Conditions is an array of current node conditions. Conditions []NodeCondition `json:"conditions,omitempty" description:"conditions is an array of current node conditions"` + // Queried from cloud provider, if available. + Addresses []NodeAddress `json:"addresses,omitempty" description:"list of addresses reachable to the node"` + // NodeSystemInfo is a set of ids/uuids to uniquely identify the node + NodeInfo NodeSystemInfo `json:"nodeInfo,omitempty" description:"node identity is a set of ids/uuids to uniquely identify the node"` } +// NodeInfo is the information collected on the node. +type NodeInfo struct { + TypeMeta `json:",inline"` + // Capacity represents the available resources. + Capacity ResourceList `json:"capacity,omitempty" description:"resource capacity of a node represented as a map of resource name to quantity of resource"` + // NodeSystemInfo is a set of ids/uuids to uniquely identify the node + NodeSystemInfo `json:",inline,omitempty" description:"node identity is a set of ids/uuids to uniquely identify the node"` +} + +// Described the current lifecycle phase of a node. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/node.md#node-phase type NodePhase string // These are the valid phases of node. @@ -601,6 +699,9 @@ const ( NodeTerminated NodePhase = "Terminated" ) +// Describes the condition of a running node. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/node.md#node-condition type NodeConditionKind string // These are valid conditions of node. Currently, we don't have enough information to decide @@ -611,8 +712,13 @@ const ( NodeReachable NodeConditionKind = "Reachable" // NodeReady means the node returns StatusOK for HTTP health check. NodeReady NodeConditionKind = "Ready" + // NodeSchedulable means the node is ready to accept new pods. + NodeSchedulable NodeConditionKind = "Schedulable" ) +// Described the conditions of a running node. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/node.md#node-condition type NodeCondition struct { Kind NodeConditionKind `json:"kind" description:"kind of the condition, one of Reachable, Ready"` Status ConditionStatus `json:"status" description:"status of the condition, one of Full, None, Unknown"` @@ -622,8 +728,23 @@ type NodeCondition struct { Message string `json:"message,omitempty" description:"human readable message indicating details about last transition"` } +type NodeAddressType string + +// These are valid address types of node. +const ( + NodeHostName NodeAddressType = "Hostname" + NodeExternalIP NodeAddressType = "ExternalIP" + NodeInternalIP NodeAddressType = "InternalIP" +) + +type NodeAddress struct { + Type NodeAddressType `json:"type" description:"type of the node address, e.g. external ip, internal ip, hostname, etc"` + Address string `json:"address" description:"string representation of the address"` +} + // NodeResources represents resources on a Kubernetes system node -// see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md for more details. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md type NodeResources struct { // Capacity represents the available resources. Capacity ResourceList `json:"capacity,omitempty" description:"resource capacity of a node represented as a map of resource name to quantity of resource"` @@ -642,18 +763,25 @@ type ResourceList map[ResourceName]util.IntOrString // Minion is a worker node in Kubernetenes. // The name of the minion according to etcd is in ID. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/node.md#node-condition type Minion struct { TypeMeta `json:",inline"` + // DEPRECATED: Use Status.Addresses instead. // Queried from cloud provider, if available. HostIP string `json:"hostIP,omitempty" description:"IP address of the node"` - // Pod IP range assigned to the node - PodCIDR string `json:"cidr,omitempty" description:"IP range assigned to the node"` // Resources available on the node NodeResources NodeResources `json:"resources,omitempty" description:"characterization of node resources"` + // Pod IP range assigned to the node + PodCIDR string `json:"podCIDR,omitempty" description:"IP range assigned to the node"` + // Unschedulable controls node schedulability of new pods. By default node is schedulable. + Unschedulable bool `json:"unschedulable,omitempty" description:"disable pod scheduling on the node"` // Status describes the current status of a node Status NodeStatus `json:"status,omitempty" description:"current status of node"` // Labels for the node Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize minions; labels of a minion assigned by the scheduler must match the scheduled pod's nodeSelector"` + // External ID of the node + ExternalID string `json:"externalID,omitempty" description:"external id of the node assigned by some machine database (e.g. a cloud provider)"` } // MinionList is a list of minions. @@ -668,10 +796,24 @@ type NamespaceSpec struct { // NamespaceStatus is information about the current status of a Namespace. type NamespaceStatus struct { + // Phase is the current lifecycle phase of the namespace. + Phase NamespacePhase `json:"phase,omitempty" description:"phase is the current lifecycle phase of the namespace"` } +type NamespacePhase string + +// These are the valid phases of a namespace. +const ( + // NamespaceActive means the namespace is available for use in the system + NamespaceActive NamespacePhase = "Active" + // NamespaceTerminating means the namespace is undergoing graceful termination + NamespaceTerminating NamespacePhase = "Terminating" +) + // A namespace provides a scope for Names. -// Use of multiple namespaces is optional +// Use of multiple namespaces is optional. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/namespaces.md type Namespace struct { TypeMeta `json:",inline"` @@ -682,7 +824,7 @@ type Namespace struct { Spec NamespaceSpec `json:"spec,omitempty" description:"spec defines the behavior of the Namespace"` // Status describes the current status of a Namespace - Status NamespaceStatus `json:"status,omitempty" description:"status describes the current status of a Namespace"` + Status NamespaceStatus `json:"status,omitempty" description:"status describes the current status of a Namespace; read-only"` } // NamespaceList is a list of Namespaces. @@ -700,6 +842,16 @@ type Binding struct { Host string `json:"host" description:"host to which to bind the specified pod"` } +// DeleteOptions may be provided when deleting an API object +type DeleteOptions struct { + TypeMeta `json:",inline"` + + // Optional duration in seconds before the object should be deleted. Value must be non-negative integer. + // The value zero indicates delete immediately. If this value is nil, the default grace period for the + // specified type will be used. + GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` +} + // Status is a return value for calls that don't return other objects. // TODO: this could go in apiserver, but I'm including it here so clients needn't // import both. @@ -859,7 +1011,7 @@ type ObjectReference struct { ID string `json:"name,omitempty" description:"id of the referent"` UID types.UID `json:"uid,omitempty" description:"uid of the referent"` APIVersion string `json:"apiVersion,omitempty" description:"API version of the referent"` - ResourceVersion string `json:"resourceVersion,omitempty" description:"specific resourceVersion to which this reference is made, if any"` + ResourceVersion string `json:"resourceVersion,omitempty" description:"specific resourceVersion to which this reference is made, if any: https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#concurrency-control-and-consistency"` // Optional. If referring to a piece of an object instead of an entire object, this string // should contain information to identify the sub-object. For example, if the object @@ -874,6 +1026,8 @@ type ObjectReference struct { // Event is a report of an event somewhere in the cluster. // TODO: Decide whether to store these separately or with the object they apply to. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/pod-states.md#events type Event struct { TypeMeta `json:",inline"` @@ -931,26 +1085,26 @@ type EventList struct { // ContainerManifest corresponds to the Container Manifest format, documented at: // https://developers.google.com/compute/docs/containers/container_vms#container_manifest // This is used as the representation of Kubernetes workloads. -// DEPRECATED: Replaced with BoundPod +// DEPRECATED: Replaced with Pod type ContainerManifest struct { // Required: This must be a supported version string, such as "v1beta1". Version string `json:"version" description:"manifest version; must be v1beta1"` // Required: This must be a DNS_SUBDOMAIN. // TODO: ID on Manifest is deprecated and will be removed in the future. - ID string `json:"id" description:"manifest name; must be a DNS_SUBDOMAIN"` + ID string `json:"id" description:"manifest name; must be a DNS_SUBDOMAIN; cannot be updated"` // TODO: UUID on Manifext is deprecated in the future once we are done // with the API refactory. It is required for now to determine the instance // of a Pod. - UUID types.UID `json:"uuid,omitempty" description:"manifest UUID"` + UUID types.UID `json:"uuid,omitempty" description:"manifest UUID; cannot be updated"` Volumes []Volume `json:"volumes" description:"list of volumes that can be mounted by containers belonging to the pod"` - Containers []Container `json:"containers" description:"list of containers belonging to the pod"` + Containers []Container `json:"containers" description:"list of containers belonging to the pod; cannot be updated; containers cannot currently be added or removed"` RestartPolicy RestartPolicy `json:"restartPolicy,omitempty" description:"restart policy for all containers within the pod; one of RestartPolicyAlways, RestartPolicyOnFailure, RestartPolicyNever"` // Optional: Set DNS policy. Defaults to "ClusterFirst" DNSPolicy DNSPolicy `json:"dnsPolicy,omitempty" description:"DNS policy for containers within the pod; one of 'ClusterFirst' or 'Default'"` } // ContainerManifestList is used to communicate container manifests to kubelet. -// DEPRECATED: Replaced with BoundPods +// DEPRECATED: Replaced with PodList type ContainerManifestList struct { TypeMeta `json:",inline"` Items []ContainerManifest `json:"items" description:"list of pod container manifests"` @@ -974,8 +1128,9 @@ const ( // PodSpec is a description of a pod type PodSpec struct { - Volumes []Volume `json:"volumes" description:"list of volumes that can be mounted by containers belonging to the pod"` - Containers []Container `json:"containers" description:"list of containers belonging to the pod"` + Volumes []Volume `json:"volumes" description:"list of volumes that can be mounted by containers belonging to the pod"` + // Required: there must be at least one container in a pod. + Containers []Container `json:"containers" description:"list of containers belonging to the pod; containers cannot currently be added or removed; there must be at least one container in a Pod"` RestartPolicy RestartPolicy `json:"restartPolicy,omitempty" description:"restart policy for all containers within the pod; one of RestartPolicyAlways, RestartPolicyOnFailure, RestartPolicyNever"` // Optional: Set DNS policy. Defaults to "ClusterFirst" DNSPolicy DNSPolicy `json:"dnsPolicy,omitempty" description:"DNS policy for containers within the pod; one of 'ClusterFirst' or 'Default'"` @@ -988,28 +1143,6 @@ type PodSpec struct { Host string `json:"host,omitempty" description:"host requested for this pod"` } -// BoundPod is a collection of containers that should be run on a host. A BoundPod -// defines how a Pod may change after a Binding is created. A Pod is a request to -// execute a pod, whereas a BoundPod is the specification that would be run on a server. -type BoundPod struct { - TypeMeta `json:",inline"` - - // Spec defines the behavior of a pod. - Spec PodSpec `json:"spec,omitempty" description:"specification of the desired state of containers and volumes comprising the pod"` -} - -// BoundPods is a list of Pods bound to a common server. The resource version of -// the pod list is guaranteed to only change when the list of bound pods changes. -type BoundPods struct { - TypeMeta `json:",inline"` - - // Host is the name of a node that these pods were bound to. - Host string `json:"host" description:"name of a node that these pods were bound to"` - - // Items is the list of all pods bound to a given host. - Items []BoundPod `json:"items" description:"list of all pods bound to a given host"` -} - // List holds a list of objects, which may not be known by the server. type List struct { TypeMeta `json:",inline"` @@ -1087,7 +1220,7 @@ type ResourceQuotaStatus struct { // ResourceQuota sets aggregate quota restrictions enforced per namespace type ResourceQuota struct { TypeMeta `json:",inline"` - + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize resource quotas"` // Spec defines the desired quota Spec ResourceQuotaSpec `json:"spec,omitempty" description:"spec defines the desired quota"` @@ -1095,15 +1228,6 @@ type ResourceQuota struct { Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` } -// ResourceQuotaUsage captures system observed quota status per namespace -// It is used to enforce atomic updates of a backing ResourceQuota.Status field in storage -type ResourceQuotaUsage struct { - TypeMeta `json:",inline"` - - // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` -} - // ResourceQuotaList is a list of ResourceQuota items type ResourceQuotaList struct { TypeMeta `json:",inline"` @@ -1112,8 +1236,23 @@ type ResourceQuotaList struct { Items []ResourceQuota `json:"items" description:"items is a list of ResourceQuota objects"` } +// NFSVolumeSource represents an NFS mount that lasts the lifetime of a pod +type NFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `json:"server" description:"the hostname or IP address of the NFS server"` + + // Path is the exported NFS share + Path string `json:"path" description:"the path that is exported by the NFS server"` + + // Optional: Defaults to false (read/write). ReadOnly here will force + // the NFS export to be mounted with read-only permissions + ReadOnly bool `json:"readOnly,omitempty" description:"forces the NFS export to be mounted with read-only permissions"` +} + // Secret holds secret data of a certain type. The total bytes of the values in // the Data field must be less than MaxSecretSize bytes. +// +// https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/design/secrets.md type Secret struct { TypeMeta `json:",inline"` diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/conversion.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/conversion.go new file mode 100644 index 000000000000..40245f812fb7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/conversion.go @@ -0,0 +1,63 @@ +/* +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 v1beta3 + +import ( + "fmt" + + newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +func init() { + // Add field conversion funcs. + err := newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "pods", + func(label, value string) (string, string, error) { + switch label { + case "name", + "status.phase", + "spec.host": + return label, value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } + err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "events", + func(label, value string) (string, string, error) { + switch label { + case "involvedObject.kind", + "involvedObject.namespace", + "involvedObject.name", + "involvedObject.uid", + "involvedObject.apiVersion", + "involvedObject.resourceVersion", + "involvedObject.fieldPath", + "reason", + "source": + return label, value, nil + default: + return "", "", fmt.Errorf("field label not supported: %s", label) + } + }) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/defaults.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/defaults.go index 55c867c3614d..d9d74d78b20a 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/defaults.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/defaults.go @@ -26,13 +26,13 @@ import ( func init() { api.Scheme.AddDefaultingFuncs( func(obj *Volume) { - if util.AllPtrFieldsNil(&obj.Source) { - obj.Source = VolumeSource{ + if util.AllPtrFieldsNil(&obj.VolumeSource) { + obj.VolumeSource = VolumeSource{ EmptyDir: &EmptyDirVolumeSource{}, } } }, - func(obj *Port) { + func(obj *ContainerPort) { if obj.Protocol == "" { obj.Protocol = ProtocolTCP } @@ -52,11 +52,6 @@ func init() { obj.TerminationMessagePath = TerminationMessagePathDefault } }, - func(obj *RestartPolicy) { - if util.AllPtrFieldsNil(obj) { - obj.Always = &RestartPolicyAlways{} - } - }, func(obj *Service) { if obj.Spec.Protocol == "" { obj.Spec.Protocol = ProtocolTCP @@ -69,6 +64,9 @@ func init() { if obj.DNSPolicy == "" { obj.DNSPolicy = DNSClusterFirst } + if obj.RestartPolicy == "" { + obj.RestartPolicy = RestartPolicyAlways + } }, func(obj *Probe) { if obj.TimeoutSeconds == 0 { @@ -85,5 +83,21 @@ func init() { obj.Protocol = "TCP" } }, + func(obj *HTTPGetAction) { + if obj.Path == "" { + obj.Path = "/" + } + }, + func(obj *ServiceSpec) { + if obj.ContainerPort.Kind == util.IntstrInt && obj.ContainerPort.IntVal == 0 || + obj.ContainerPort.Kind == util.IntstrString && obj.ContainerPort.StrVal == "" { + obj.ContainerPort = util.NewIntOrStringFromInt(obj.Port) + } + }, + func(obj *NamespaceStatus) { + if obj.Phase == "" { + obj.Phase = NamespaceActive + } + }, ) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/defaults_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/defaults_test.go index e1ebf388b1e9..a4bac78f2f7d 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/defaults_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/defaults_test.go @@ -22,6 +22,7 @@ import ( current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { @@ -51,48 +52,6 @@ func TestSetDefaultService(t *testing.T) { } } -func TestSetDefaulPodSpec(t *testing.T) { - bp := ¤t.BoundPod{} - bp.Spec.Volumes = []current.Volume{{}} - - obj2 := roundTrip(t, runtime.Object(bp)) - bp2 := obj2.(*current.BoundPod) - if bp2.Spec.DNSPolicy != current.DNSClusterFirst { - t.Errorf("Expected default dns policy :%s, got: %s", current.DNSClusterFirst, bp2.Spec.DNSPolicy) - } - policy := bp2.Spec.RestartPolicy - if policy.Never != nil || policy.OnFailure != nil || policy.Always == nil { - t.Errorf("Expected only policy.Always is set, got: %s", policy) - } - vsource := bp2.Spec.Volumes[0].Source - if vsource.EmptyDir == nil { - t.Errorf("Expected non-empty volume is set, got: %s", vsource.EmptyDir) - } -} - -func TestSetDefaultContainer(t *testing.T) { - bp := ¤t.BoundPod{} - bp.Spec.Containers = []current.Container{{}} - bp.Spec.Containers[0].Ports = []current.Port{{}} - - obj2 := roundTrip(t, runtime.Object(bp)) - bp2 := obj2.(*current.BoundPod) - - container := bp2.Spec.Containers[0] - if container.TerminationMessagePath != current.TerminationMessagePathDefault { - t.Errorf("Expected termination message path: %s, got: %s", - current.TerminationMessagePathDefault, container.TerminationMessagePath) - } - if container.ImagePullPolicy != current.PullIfNotPresent { - t.Errorf("Expected image pull policy: %s, got: %s", - current.PullIfNotPresent, container.ImagePullPolicy) - } - if container.Ports[0].Protocol != current.ProtocolTCP { - t.Errorf("Expected protocol: %s, got: %s", - current.ProtocolTCP, container.Ports[0].Protocol) - } -} - func TestSetDefaultSecret(t *testing.T) { s := ¤t.Secret{} obj2 := roundTrip(t, runtime.Object(s)) @@ -112,3 +71,29 @@ func TestSetDefaulEndpointsProtocol(t *testing.T) { t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Protocol) } } + +func TestSetDefaulServiceDestinationPort(t *testing.T) { + in := ¤t.Service{Spec: current.ServiceSpec{Port: 1234}} + obj := roundTrip(t, runtime.Object(in)) + out := obj.(*current.Service) + if out.Spec.ContainerPort.Kind != util.IntstrInt || out.Spec.ContainerPort.IntVal != 1234 { + t.Errorf("Expected ContainerPort to be defaulted, got %s", out.Spec.ContainerPort) + } + + in = ¤t.Service{Spec: current.ServiceSpec{Port: 1234, ContainerPort: util.NewIntOrStringFromInt(5678)}} + obj = roundTrip(t, runtime.Object(in)) + out = obj.(*current.Service) + if out.Spec.ContainerPort.Kind != util.IntstrInt || out.Spec.ContainerPort.IntVal != 5678 { + t.Errorf("Expected ContainerPort to be unchanged, got %s", out.Spec.ContainerPort) + } +} + +func TestSetDefaultNamespace(t *testing.T) { + s := ¤t.Namespace{} + obj2 := roundTrip(t, runtime.Object(s)) + s2 := obj2.(*current.Namespace) + + if s2.Status.Phase != current.NamespaceActive { + t.Errorf("Expected phase %v, got %v", current.NamespaceActive, s2.Status.Phase) + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/register.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/register.go index 93217e99d192..ef0ad07c5d4e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/register.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/register.go @@ -31,8 +31,6 @@ func init() { &PodStatusResult{}, &PodTemplate{}, &PodTemplateList{}, - &BoundPod{}, - &BoundPods{}, &ReplicationController{}, &ReplicationControllerList{}, &Service{}, @@ -40,6 +38,7 @@ func init() { &Endpoints{}, &EndpointsList{}, &Node{}, + &NodeInfo{}, &NodeList{}, &Binding{}, &Status{}, @@ -50,11 +49,11 @@ func init() { &LimitRangeList{}, &ResourceQuota{}, &ResourceQuotaList{}, - &ResourceQuotaUsage{}, &Namespace{}, &NamespaceList{}, &Secret{}, &SecretList{}, + &DeleteOptions{}, ) // Legacy names are supported api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) @@ -66,8 +65,6 @@ func (*PodList) IsAnAPIObject() {} func (*PodStatusResult) IsAnAPIObject() {} func (*PodTemplate) IsAnAPIObject() {} func (*PodTemplateList) IsAnAPIObject() {} -func (*BoundPod) IsAnAPIObject() {} -func (*BoundPods) IsAnAPIObject() {} func (*ReplicationController) IsAnAPIObject() {} func (*ReplicationControllerList) IsAnAPIObject() {} func (*Service) IsAnAPIObject() {} @@ -75,6 +72,7 @@ func (*ServiceList) IsAnAPIObject() {} func (*Endpoints) IsAnAPIObject() {} func (*EndpointsList) IsAnAPIObject() {} func (*Node) IsAnAPIObject() {} +func (*NodeInfo) IsAnAPIObject() {} func (*NodeList) IsAnAPIObject() {} func (*Binding) IsAnAPIObject() {} func (*Status) IsAnAPIObject() {} @@ -85,8 +83,8 @@ func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} -func (*ResourceQuotaUsage) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {} +func (*DeleteOptions) IsAnAPIObject() {} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/types.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/types.go index 2e79c5e19d67..2ddc3e1ce4f0 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/types.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3/types.go @@ -52,25 +52,25 @@ import ( type TypeMeta struct { // Kind is a string value representing the REST resource this object represents. // Servers may infer this from the endpoint the client submits requests to. - Kind string `json:"kind,omitempty"` + Kind string `json:"kind,omitempty" description:"kind of object, in CamelCase; cannot be updated"` // APIVersion defines the versioned schema of this representation of an object. // Servers should convert recognized schemas to the latest internal value, and // may reject unrecognized values. - APIVersion string `json:"apiVersion,omitempty"` + APIVersion string `json:"apiVersion,omitempty" description:"version of the schema the object should have"` } // ListMeta describes metadata that synthetic resources must have, including lists and // various status objects. type ListMeta struct { // SelfLink is a URL representing this object. - SelfLink string `json:"selfLink,omitempty"` + SelfLink string `json:"selfLink,omitempty" description:"URL for the object; populated by the system, read-only"` // An opaque value that represents the version of this response for use with optimistic // concurrency and change monitoring endpoints. Clients must treat these values as opaque // and values may only be valid for a particular resource or set of resources. Only servers // will generate resource versions. - ResourceVersion string `json:"resourceVersion,omitempty"` + ResourceVersion string `json:"resourceVersion,omitempty" description:"string that identifies the internal version of this object that can be used by clients to determine when objects have changed; populated by the system, read-only; value must be treated as opaque by clients and passed unmodified back to the server: https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#concurrency-control-and-consistency"` } // ObjectMeta is metadata that all persisted resources must have, which includes all objects @@ -80,7 +80,7 @@ type ObjectMeta struct { // some resources may allow a client to request the generation of an appropriate name // automatically. Name is primarily intended for creation idempotence and configuration // definition. - Name string `json:"name,omitempty"` + Name string `json:"name,omitempty" description:"string that identifies an object. Must be unique within a namespace; cannot be updated"` // GenerateName indicates that the name should be made unique by the server prior to persisting // it. A non-empty value for the field indicates the name will be made unique (and the name @@ -99,35 +99,46 @@ type ObjectMeta struct { // equivalent to the "default" namespace, but "default" is the canonical representation. // Not all objects are required to be scoped to a namespace - the value of this field for // those objects will be empty. - Namespace string `json:"namespace,omitempty"` + Namespace string `json:"namespace,omitempty" description:"namespace of the object; cannot be updated"` // SelfLink is a URL representing this object. - SelfLink string `json:"selfLink,omitempty"` + SelfLink string `json:"selfLink,omitempty" description:"URL for the object; populated by the system, read-only"` // UID is the unique in time and space value for this object. It is typically generated by // the server on successful creation of a resource and is not allowed to change on PUT // operations. - UID types.UID `json:"uid,omitempty"` + UID types.UID `json:"uid,omitempty" description:"unique UUID across space and time; populated by the system; read-only"` // An opaque value that represents the version of this resource. May be used for optimistic // concurrency, change detection, and the watch operation on a resource or set of resources. // Clients must treat these values as opaque and values may only be valid for a particular // resource or set of resources. Only servers will generate resource versions. - ResourceVersion string `json:"resourceVersion,omitempty"` + ResourceVersion string `json:"resourceVersion,omitempty" description:"string that identifies the internal version of this object that can be used by clients to determine when objects have changed; populated by the system, read-only; value must be treated as opaque by clients and passed unmodified back to the server: https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#concurrency-control-and-consistency"` // CreationTimestamp is a timestamp representing the server time when this object was // created. It is not guaranteed to be set in happens-before order across separate operations. // Clients may not set this value. It is represented in RFC3339 form and is in UTC. - CreationTimestamp util.Time `json:"creationTimestamp,omitempty"` + CreationTimestamp util.Time `json:"creationTimestamp,omitempty" description:"RFC 3339 date and time at which the object was created; populated by the system, read-only; null for lists"` + + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *util.Time `json:"deletionTimestamp,omitempty" description:"RFC 3339 date and time at which the object will be deleted; populated by the system when a graceful deletion is requested, read-only; if not set, graceful deletion of the object has not been requested"` // Labels are key value pairs that may be used to scope and select individual resources. // TODO: replace map[string]string with labels.LabelSet type - Labels map[string]string `json:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize objects; may match selectors of replication controllers and services"` // Annotations are unstructured key value data stored with a resource that may be set by // external tooling. They are not queryable and should be preserved when modifying // objects. - Annotations map[string]string `json:"annotations,omitempty"` + Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about objects"` } const ( @@ -173,11 +184,11 @@ const ( type Volume struct { // Required: This must be a DNS_LABEL. Each volume in a pod must have // a unique name. - Name string `json:"name"` + Name string `json:"name" description:"volume name; must be a DNS_LABEL and unique within the pod"` // Source represents the location and type of a volume to mount. // This is optional for now. If not specified, the Volume is implied to be an EmptyDir. // This implied behavior is deprecated and will be removed in a future version. - Source VolumeSource `json:"source,omitempty"` + VolumeSource `json:",inline,omitempty"` } // VolumeSource represents the source location of a valume to mount. @@ -189,24 +200,38 @@ type VolumeSource struct { // to see the host machine. Most containers will NOT need this. // TODO(jonesdl) We need to restrict who can use host directory mounts and who can/can not // mount host directories as read/write. - HostPath *HostPathVolumeSource `json:"hostPath"` + HostPath *HostPathVolumeSource `json:"hostPath" description:"pre-existing host file or directory; generally for privileged system daemons or other agents tied to the host"` // EmptyDir represents a temporary directory that shares a pod's lifetime. - EmptyDir *EmptyDirVolumeSource `json:"emptyDir"` + EmptyDir *EmptyDirVolumeSource `json:"emptyDir" description:"temporary directory that shares a pod's lifetime"` // GCEPersistentDisk represents a GCE Disk resource that is attached to a // kubelet's host machine and then exposed to the pod. - GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk"` + GCEPersistentDisk *GCEPersistentDiskVolumeSource `json:"gcePersistentDisk" description:"GCE disk resource attached to the host machine on demand"` // GitRepo represents a git repository at a particular revision. - GitRepo *GitRepoVolumeSource `json:"gitRepo"` + GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` // Secret represents a secret that should populate this volume. - Secret *SecretVolumeSource `json:"secret"` + Secret *SecretVolumeSource `json:"secret" description:"secret to populate volume"` + // NFS represents an NFS mount on the host that shares a pod's lifetime + NFS *NFSVolumeSource `json:"nfs" description:"NFS volume that will be mounted in the host machine"` } // HostPathVolumeSource represents bare host directory volume. type HostPathVolumeSource struct { - Path string `json:"path"` + Path string `json:"path" description:"path of the directory on the host"` } -type EmptyDirVolumeSource struct{} +type EmptyDirVolumeSource struct { + // Optional: what type of storage medium should back this directory. + // The default is "" which means to use the node's default medium. + Medium StorageType `json:"medium" description:"type of storage used to back the volume; must be an empty string (default) or Memory"` +} + +// StorageType defines ways that storage can be allocated to a volume. +type StorageType string + +const ( + StorageTypeDefault StorageType = "" // use whatever the default is for the node + StorageTypeMemory StorageType = "Memory" // use memory (tmpfs) +) // Protocol defines network protocols supported for things like conatiner ports. type Protocol string @@ -225,27 +250,27 @@ const ( // A GCE PD can only be mounted as read/write once. type GCEPersistentDiskVolumeSource struct { // Unique name of the PD resource. Used to identify the disk in GCE - PDName string `json:"pdName"` + PDName string `json:"pdName" description:"unique name of the PD resource in GCE"` // Required: Filesystem type to mount. // Must be a filesystem type supported by the host operating system. // Ex. "ext4", "xfs", "ntfs" // TODO: how do we prevent errors in the filesystem from compromising the machine - FSType string `json:"fsType,omitempty"` + FSType string `json:"fsType,omitempty" description:"file system type to mount, such as ext4, xfs, ntfs"` // Optional: Partition on the disk to mount. // If omitted, kubelet will attempt to mount the device name. // Ex. For /dev/sda1, this field is "1", for /dev/sda, this field is 0 or empty. - Partition int `json:"partition,omitempty"` + Partition int `json:"partition,omitempty" description:"partition on the disk to mount (e.g., '1' for /dev/sda1); if omitted the plain device name (e.g., /dev/sda) will be mounted"` // Optional: Defaults to false (read/write). ReadOnly here will force // the ReadOnly setting in VolumeMounts. - ReadOnly bool `json:"readOnly,omitempty"` + ReadOnly bool `json:"readOnly,omitempty" description:"read-only if true, read-write otherwise (false or unspecified)"` } // GitRepoVolumeSource represents a volume that is pulled from git when the pod is created. type GitRepoVolumeSource struct { // Repository URL - Repository string `json:"repository"` + Repository string `json:"repository" description:"repository URL"` // Commit hash, this is optional - Revision string `json:"revision"` + Revision string `json:"revision" description:"commit hash for the specified revision"` } // SecretVolumeSource adapts a Secret into a VolumeSource @@ -254,53 +279,66 @@ type SecretVolumeSource struct { Target ObjectReference `json:"target" description:"target is a reference to a secret"` } -// Port represents a network port in a single container. -type Port struct { +// NFSVolumeSource represents an NFS mount that lasts the lifetime of a pod +type NFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `json:"server" description:"the hostname or IP address of the NFS server"` + + // Path is the exported NFS share + Path string `json:"path" description:"the path that is exported by the NFS server"` + + // Optional: Defaults to false (read/write). ReadOnly here will force + // the NFS export to be mounted with read-only permissions + ReadOnly bool `json:"readOnly,omitempty" description:"forces the NFS export to be mounted with read-only permissions"` +} + +// ContainerPort represents a network port in a single container. +type ContainerPort struct { // Optional: If specified, this must be a DNS_LABEL. Each named port // in a pod must have a unique name. - Name string `json:"name,omitempty"` + Name string `json:"name,omitempty" description:"name for the port that can be referred to by services; must be a DNS_LABEL and unique without the pod"` // Optional: If specified, this must be a valid port number, 0 < x < 65536. - HostPort int `json:"hostPort,omitempty"` + HostPort int `json:"hostPort,omitempty" description:"number of port to expose on the host; most containers do not need this"` // Required: This must be a valid port number, 0 < x < 65536. - ContainerPort int `json:"containerPort"` - // Optional: Supports "TCP" and "UDP". Defaults to "TCP". - Protocol Protocol `json:"protocol,omitempty"` + ContainerPort int `json:"containerPort" description:"number of port to expose on the pod's IP address"` + // Optional: Defaults to "TCP". + Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"` // Optional: What host IP to bind the external port to. - HostIP string `json:"hostIP,omitempty"` + HostIP string `json:"hostIP,omitempty" description:"host IP to bind the port to"` } // VolumeMount describes a mounting of a Volume within a container. type VolumeMount struct { // Required: This must match the Name of a Volume [above]. - Name string `json:"name"` + Name string `json:"name" description:"name of the volume to mount"` // Optional: Defaults to false (read-write). - ReadOnly bool `json:"readOnly,omitempty"` + ReadOnly bool `json:"readOnly,omitempty" description:"mounted read-only if true, read-write otherwise (false or unspecified)"` // Required. - MountPath string `json:"mountPath"` + MountPath string `json:"mountPath" description:"path within the container at which the volume should be mounted"` } // EnvVar represents an environment variable present in a Container. type EnvVar struct { // Required: This must be a C_IDENTIFIER. - Name string `json:"name"` + Name string `json:"name" description:"name of the environment variable; must be a C_IDENTIFIER"` // Optional: defaults to "". - Value string `json:"value,omitempty"` + Value string `json:"value,omitempty" description:"value of the environment variable; defaults to empty string"` } // HTTPGetAction describes an action based on HTTP Get requests. type HTTPGetAction struct { // Optional: Path to access on the HTTP server. - Path string `json:"path,omitempty"` + Path string `json:"path,omitempty" description:"path to access on the HTTP server"` // Required: Name or number of the port to access on the container. - Port util.IntOrString `json:"port,omitempty"` + Port util.IntOrString `json:"port,omitempty" description:"number or name of the port to access on the container"` // Optional: Host name to connect to, defaults to the pod IP. - Host string `json:"host,omitempty"` + Host string `json:"host,omitempty" description:"hostname to connect to; defaults to pod IP"` } // TCPSocketAction describes an action based on opening a socket type TCPSocketAction struct { // Required: Port to connect to. - Port util.IntOrString `json:"port,omitempty"` + Port util.IntOrString `json:"port,omitempty" description:"number of name of the port to access on the container"` } // ExecAction describes a "run in container" action. @@ -309,7 +347,7 @@ type ExecAction struct { // command is root ('/') in the container's filesystem. The command is simply exec'd, it is // not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use // a shell, you need to explicitly call out to that shell. - Command []string `json:"command,omitempty"` + Command []string `json:"command,omitempty" description:"command line to execute inside the container; working directory for the command is root ('/') in the container's file system; the command is exec'd, not run inside a shell; exit status of 0 is treated as live/healthy and non-zero is unhealthy"` } // Probe describes a liveness probe to be examined to the container. @@ -317,9 +355,9 @@ type Probe struct { // The action taken to determine the health of a container Handler `json:",inline"` // Length of time before health checking is activated. In seconds. - InitialDelaySeconds int64 `json:"initialDelaySeconds,omitempty"` + InitialDelaySeconds int64 `json:"initialDelaySeconds,omitempty" description:"number of seconds after the container has started before liveness probes are initiated"` // Length of time before health checking times out. In seconds. - TimeoutSeconds int64 `json:"timeoutSeconds,omitempty"` + TimeoutSeconds int64 `json:"timeoutSeconds,omitempty" description:"number of seconds after which liveness probes timeout; defaults to 1 second"` } // PullPolicy describes a policy for if/when to pull a container image @@ -340,9 +378,9 @@ type CapabilityType string // Capabilities represent POSIX capabilities that can be added or removed to a running container. type Capabilities struct { // Added capabilities - Add []CapabilityType `json:"add,omitempty"` + Add []CapabilityType `json:"add,omitempty" description:"added capabilities"` // Removed capabilities - Drop []CapabilityType `json:"drop,omitempty"` + Drop []CapabilityType `json:"drop,omitempty" description:"droped capabilities"` } // ResourceRequirements describes the compute resource requirements. @@ -360,28 +398,28 @@ const ( type Container struct { // Required: This must be a DNS_LABEL. Each container in a pod must // have a unique name. - Name string `json:"name"` + Name string `json:"name" description:"name of the container; must be a DNS_LABEL and unique within the pod; cannot be updated"` // Required. - Image string `json:"image"` + Image string `json:"image" description:"Docker image name"` // Optional: Defaults to whatever is defined in the image. - Command []string `json:"command,omitempty"` + Command []string `json:"command,omitempty" description:"command argv array; not executed within a shell; defaults to entrypoint or command in the image; cannot be updated"` // Optional: Defaults to Docker's default. - WorkingDir string `json:"workingDir,omitempty"` - Ports []Port `json:"ports,omitempty"` - Env []EnvVar `json:"env,omitempty"` - Resources ResourceRequirements `json:"resources,omitempty" description:"Compute Resources required by this container"` - VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` - LivenessProbe *Probe `json:"livenessProbe,omitempty"` - ReadinessProbe *Probe `json:"readinessProbe,omitempty"` - Lifecycle *Lifecycle `json:"lifecycle,omitempty"` + WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default; cannot be updated"` + Ports []ContainerPort `json:"ports,omitempty" description:"list of ports to expose from the container; cannot be updated"` + Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container; cannot be updated"` + Resources ResourceRequirements `json:"resources,omitempty" description:"Compute Resources required by this container; cannot be updated"` + VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesyste; cannot be updated"` + LivenessProbe *Probe `json:"livenessProbe,omitempty" description:"periodic probe of container liveness; container will be restarted if the probe fails; cannot be updated"` + ReadinessProbe *Probe `json:"readinessProbe,omitempty" description:"periodic probe of container service readiness; container will be removed from service endpoints if the probe fails; cannot be updated"` + Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log - TerminationMessagePath string `json:"terminationMessagePath,omitempty"` + TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` // Optional: Default to false. - Privileged bool `json:"privileged,omitempty"` + Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` // Optional: Policy for pulling images for this container - ImagePullPolicy PullPolicy `json:"imagePullPolicy"` + ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty"` + Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` } // Handler defines a specific action that should be taken @@ -389,12 +427,12 @@ type Container struct { type Handler struct { // One and only one of the following should be specified. // Exec specifies the action to take. - Exec *ExecAction `json:"exec,omitempty"` + Exec *ExecAction `json:"exec,omitempty" description:"exec-based handler"` // HTTPGet specifies the http request to perform. - HTTPGet *HTTPGetAction `json:"httpGet,omitempty"` + HTTPGet *HTTPGetAction `json:"httpGet,omitempty" description:"HTTP-based handler"` // TCPSocket specifies an action involving a TCP port. // TODO: implement a realistic TCP lifecycle hook - TCPSocket *TCPSocketAction `json:"tcpSocket,omitempty"` + TCPSocket *TCPSocketAction `json:"tcpSocket,omitempty" description:"TCP-based handler; TCP hooks not yet supported"` } // Lifecycle describes actions that the management system should take in response to container lifecycle @@ -403,10 +441,10 @@ type Handler struct { type Lifecycle struct { // PostStart is called immediately after a container is created. If the handler fails, the container // is terminated and restarted. - PostStart *Handler `json:"postStart,omitempty"` + PostStart *Handler `json:"postStart,omitempty" description:"called immediately after a container is started; if the handler fails, the container is terminated and restarted according to its restart policy; other management of the container blocks until the hook completes"` // PreStop is called immediately before a container is terminated. The reason for termination is // passed to the handler. Regardless of the outcome of the handler, the container is eventually terminated. - PreStop *Handler `json:"preStop,omitempty"` + PreStop *Handler `json:"preStop,omitempty" description:"called before a container is terminated; the container is terminated after the handler completes; other management of the container blocks until the hook completes"` } type ConditionStatus string @@ -423,48 +461,44 @@ const ( type ContainerStateWaiting struct { // Reason could be pulling image, - Reason string `json:"reason,omitempty"` + Reason string `json:"reason,omitempty" description:"(brief) reason the container is not yet running, such as pulling its image"` } type ContainerStateRunning struct { - StartedAt util.Time `json:"startedAt,omitempty"` + StartedAt util.Time `json:"startedAt,omitempty" description:"time at which the container was last (re-)started"` } type ContainerStateTerminated struct { - ExitCode int `json:"exitCode"` - Signal int `json:"signal,omitempty"` - Reason string `json:"reason,omitempty"` - Message string `json:"message,omitempty"` - StartedAt util.Time `json:"startedAt,omitempty"` - FinishedAt util.Time `json:"finishedAt,omitempty"` + ExitCode int `json:"exitCode" description:"exit status from the last termination of the container"` + Signal int `json:"signal,omitempty" description:"signal from the last termination of the container"` + Reason string `json:"reason,omitempty" description:"(brief) reason from the last termination of the container"` + Message string `json:"message,omitempty" description:"message regarding the last termination of the container"` + StartedAt util.Time `json:"startedAt,omitempty" description:"time at which previous execution of the container started"` + FinishedAt util.Time `json:"finishedAt,omitempty" description:"time at which the container last terminated"` } // ContainerState holds a possible state of container. // Only one of its members may be specified. // If none of them is specified, the default one is ContainerStateWaiting. type ContainerState struct { - Waiting *ContainerStateWaiting `json:"waiting,omitempty"` - Running *ContainerStateRunning `json:"running,omitempty"` - Termination *ContainerStateTerminated `json:"termination,omitempty"` + Waiting *ContainerStateWaiting `json:"waiting,omitempty" description:"details about a waiting container"` + Running *ContainerStateRunning `json:"running,omitempty" description:"details about a running container"` + Termination *ContainerStateTerminated `json:"termination,omitempty" description:"details about a terminated container"` } type ContainerStatus struct { // TODO(dchen1107): Should we rename PodStatus to a more generic name or have a separate states // defined for container? - State ContainerState `json:"state,omitempty"` - Ready bool `json:"ready"` + State ContainerState `json:"state,omitempty" description:"details about the container's current condition"` + Ready bool `json:"ready" description:"specifies whether the container has passed its readiness probe"` // Note that this is calculated from dead containers. But those containers are subject to // garbage collection. This value will get capped at 5 by GC. - RestartCount int `json:"restartCount"` - // TODO(dchen1107): Introduce our own NetworkSettings struct here? - ContainerID string `json:"containerID,omitempty" description:"container's ID in the format 'docker://'"` - // The IP of the Pod - // PodIP is deprecated and will be removed from v1beta3 once it becomes possible for the Kubelet to report PodStatus. - PodIP string `json:"podIP,omitempty"` + RestartCount int `json:"restartCount" description:"the number of times the container has been restarted, currently based on the number of dead containers that have not yet been removed"` // TODO(dchen1107): Which image the container is running with? // The image the container is running - Image string `json:"image"` - ImageID string `json:"imageID" description:"ID of the container's image"` + Image string `json:"image" description:"image of the container"` + ImageID string `json:"imageID" description:"ID of the container's image"` + ContainerID string `json:"containerID,omitempty" description:"container's ID in the format 'docker://'"` } // PodPhase is a label for the condition of a pod at the current time. @@ -490,43 +524,38 @@ const ( PodUnknown PodPhase = "Unknown" ) -// PodConditionKind is a valid value for PodCondition.Kind -type PodConditionKind string +// PodConditionType is a valid value for PodCondition.Type +type PodConditionType string // These are valid conditions of pod. const ( // PodReady means the pod is able to service requests and should be added to the // load balancing pools of all matching services. - PodReady PodConditionKind = "Ready" + PodReady PodConditionType = "Ready" ) // TODO: add LastTransitionTime, Reason, Message to match NodeCondition api. type PodCondition struct { + // Type is the type of the condition + Type PodConditionType `json:"type" description:"kind of the condition"` // Status is the status of the condition - Kind PodConditionKind `json:"kind"` - // Status is the status of the condition - Status ConditionStatus `json:"status"` + Status ConditionStatus `json:"status" description:"status of the condition, one of Full, None, Unknown"` } // PodInfo contains one entry for every container with available info. type PodInfo map[string]ContainerStatus -type RestartPolicyAlways struct{} - -// TODO(dchen1107): Define what kinds of failures should restart. -// TODO(dchen1107): Decide whether to support policy knobs, and, if so, which ones. -type RestartPolicyOnFailure struct{} - -type RestartPolicyNever struct{} +// RestartPolicy describes how the container should be restarted. +// Only one of the following restart policies may be specified. +// If none of the following policies is specified, the default one +// is RestartPolicyAlways. +type RestartPolicy string -type RestartPolicy struct { - // Only one of the following restart policies may be specified. - // If none of the following policies is specified, the default one - // is RestartPolicyAlways. - Always *RestartPolicyAlways `json:"always,omitempty"` - OnFailure *RestartPolicyOnFailure `json:"onFailure,omitempty"` - Never *RestartPolicyNever `json:"never,omitempty"` -} +const ( + RestartPolicyAlways RestartPolicy = "Always" + RestartPolicyOnFailure RestartPolicy = "OnFailure" + RestartPolicyNever RestartPolicy = "Never" +) // DNSPolicy defines how a pod's DNS will be configured. type DNSPolicy string @@ -544,33 +573,34 @@ const ( // PodSpec is a description of a pod type PodSpec struct { - Volumes []Volume `json:"volumes"` - Containers []Container `json:"containers"` - RestartPolicy RestartPolicy `json:"restartPolicy,omitempty"` + Volumes []Volume `json:"volumes" description:"list of volumes that can be mounted by containers belonging to the pod"` + // Required: there must be at least one container in a pod. + Containers []Container `json:"containers" description:"list of containers belonging to the pod; cannot be updated; containers cannot currently be added or removed; there must be at least one container in a Pod"` + RestartPolicy RestartPolicy `json:"restartPolicy,omitempty" description:"restart policy for all containers within the pod; one of RestartPolicyAlways, RestartPolicyOnFailure, RestartPolicyNever"` // Optional: Set DNS policy. Defaults to "ClusterFirst" - DNSPolicy DNSPolicy `json:"dnsPolicy,omitempty"` + DNSPolicy DNSPolicy `json:"dnsPolicy,omitempty" description:"DNS policy for containers within the pod; one of 'ClusterFirst' or 'Default'"` // NodeSelector is a selector which must be true for the pod to fit on a node - NodeSelector map[string]string `json:"nodeSelector,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` // Host is a request to schedule this pod onto a specific host. If it is non-empty, // the the scheduler simply schedules this pod onto that host, assuming that it fits - // other requirements. + // resource requirements. Host string `json:"host,omitempty" description:"host requested for this pod"` } // PodStatus represents information about the status of a pod. Status may trail the actual // state of a system. type PodStatus struct { - Phase PodPhase `json:"phase,omitempty"` - Conditions []PodCondition `json:"Condition,omitempty"` + Phase PodPhase `json:"phase,omitempty" description:"current condition of the pod."` + Conditions []PodCondition `json:"Condition,omitempty" description:"current service state of pod"` // A human readable message indicating details about why the pod is in this state. - Message string `json:"message,omitempty"` + Message string `json:"message,omitempty" description:"human readable message indicating details about why the pod is in this condition"` // Host is the name of the node that this Pod is currently bound to, or empty if no // assignment has been done. - Host string `json:"host,omitempty"` - HostIP string `json:"hostIP,omitempty"` - PodIP string `json:"podIP,omitempty"` + Host string `json:"host,omitempty" description:"host to which the pod is assigned; empty if not yet scheduled; cannot be updated"` + HostIP string `json:"hostIP,omitempty" description:"IP address of the host to which the pod is assigned; empty if not yet scheduled"` + PodIP string `json:"podIP,omitempty" description:"IP address allocated to the pod; routable at least within the cluster; empty if not yet allocated"` // The key of this map is the *name* of the container within the manifest; it has one // entry per container in the manifest. The value of this map is currently the output @@ -578,135 +608,110 @@ type PodStatus struct { // upon. // TODO: Make real decisions about what our info should look like. Re-enable fuzz test // when we have done this. - Info PodInfo `json:"info,omitempty"` + Info PodInfo `json:"info,omitempty" description:"map of container name to container status"` } // PodStatusResult is a wrapper for PodStatus returned by kubelet that can be encode/decoded type PodStatusResult struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Status represents the current information about a pod. This data may not be up // to date. - Status PodStatus `json:"status,omitempty"` + Status PodStatus `json:"status,omitempty" description:"most recently observed status of the pod; populated by the system, read-only; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // Pod is a collection of containers that can run on a host. This resource is created -// by clients and scheduled onto hosts. BoundPod represents the state of this resource -// to hosts. +// by clients and scheduled onto hosts. type Pod struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the behavior of a pod. - Spec PodSpec `json:"spec,omitempty"` + Spec PodSpec `json:"spec,omitempty" description:"specification of the desired behavior of the pod; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` // Status represents the current information about a pod. This data may not be up // to date. - Status PodStatus `json:"status,omitempty"` + Status PodStatus `json:"status,omitempty" description:"most recently observed status of the pod; populated by the system, read-only; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // PodList is a list of Pods. type PodList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#types-kinds` - Items []Pod `json:"items"` + Items []Pod `json:"items" description:"list of pods"` } // PodTemplateSpec describes the data a pod should have when created from a template type PodTemplateSpec struct { // Metadata of the pods created from this template. - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the behavior of a pod. - Spec PodSpec `json:"spec,omitempty"` + Spec PodSpec `json:"spec,omitempty" description:"specification of the desired behavior of the pod; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // PodTemplate describes a template for creating copies of a predefined pod. type PodTemplate struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the behavior of a pod. - Spec PodTemplateSpec `json:"spec,omitempty"` + Spec PodTemplateSpec `json:"spec,omitempty" description:"specification of the desired behavior of the pod; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // PodTemplateList is a list of PodTemplates. type PodTemplateList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` - - Items []PodTemplate `json:"items"` -} - -// BoundPod is a collection of containers that should be run on a host. A BoundPod -// defines how a Pod may change after a Binding is created. A Pod is a request to -// execute a pod, whereas a BoundPod is the specification that would be run on a server. -type BoundPod struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` - - // Spec defines the behavior of a pod. - Spec PodSpec `json:"spec,omitempty"` -} - -// BoundPods is a list of Pods bound to a common server. The resource version of -// the pod list is guaranteed to only change when the list of bound pods changes. -type BoundPods struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - // Host is the name of a node that these pods were bound to. - Host string `json:"host"` - - // Items is the list of all pods bound to a given host. - Items []BoundPod `json:"items"` + Items []PodTemplate `json:"items" description:"list of pod templates"` } // ReplicationControllerSpec is the specification of a replication controller. type ReplicationControllerSpec struct { // Replicas is the number of desired replicas. - Replicas int `json:"replicas"` + Replicas int `json:"replicas" description:"number of replicas desired"` // Selector is a label query over pods that should match the Replicas count. - Selector map[string]string `json:"selector,omitempty"` + Selector map[string]string `json:"selector,omitempty" description:"label keys and values that must match in order to be controlled by this replication controller"` // TemplateRef is a reference to an object that describes the pod that will be created if // insufficient replicas are detected. - TemplateRef *ObjectReference `json:"templateRef,omitempty"` + TemplateRef *ObjectReference `json:"templateRef,omitempty" description:"reference to an object that describes the pod that will be created if insufficient replicas are detected"` // Template is the object that describes the pod that will be created if // insufficient replicas are detected. This takes precedence over a // TemplateRef. - Template *PodTemplateSpec `json:"template,omitempty"` + Template *PodTemplateSpec `json:"template,omitempty" description:"object that describes the pod that will be created if insufficient replicas are detected; takes precendence over templateRef"` } // ReplicationControllerStatus represents the current status of a replication // controller. type ReplicationControllerStatus struct { // Replicas is the number of actual replicas. - Replicas int `json:"replicas"` + Replicas int `json:"replicas" description:"most recently oberved number of replicas"` } // ReplicationController represents the configuration of a replication controller. type ReplicationController struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the desired behavior of this replication controller. - Spec ReplicationControllerSpec `json:"spec,omitempty"` + Spec ReplicationControllerSpec `json:"spec,omitempty" description:"specification of the desired behavior of the replication controller; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` // Status is the current status of this replication controller. This data may be // out of date by some window of time. - Status ReplicationControllerStatus `json:"status,omitempty"` + Status ReplicationControllerStatus `json:"status,omitempty" description:"most recently observed status of the replication controller; populated by the system, read-only; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // ReplicationControllerList is a collection of replication controllers. type ReplicationControllerList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - Items []ReplicationController `json:"items"` + Items []ReplicationController `json:"items" description:"list of replication controllers"` } // Session Affinity Type string @@ -727,31 +732,33 @@ type ServiceStatus struct{} type ServiceSpec struct { // Port is the TCP or UDP port that will be made available to each pod for connecting to the pods // proxied by this service. - Port int `json:"port"` + Port int `json:"port" description:"port exposed by the service"` // Optional: Supports "TCP" and "UDP". Defaults to "TCP". - Protocol Protocol `json:"protocol,omitempty"` + Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"` // This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected. - Selector map[string]string `json:"selector"` + Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"` // PortalIP is usually assigned by the master. If specified by the user // we will try to respect it or else fail the request. This field can // not be changed by updates. - PortalIP string `json:"portalIP,omitempty"` + PortalIP string `json:"portalIP,omitempty description: IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated"` // CreateExternalLoadBalancer indicates whether a load balancer should be created for this service. - CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty"` - // PublicIPs are used by external load balancers. - PublicIPs []string `json:"publicIPs,omitempty"` + CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` + + // PublicIPs are used by external load balancers, or can be set by + // users to handle external traffic that arrives at a node. + PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` // ContainerPort is the name or number of the port on the container to direct traffic to. // This is useful if the containers the service points to have multiple open ports. - // Optional: If unspecified, the first port on the container will be used. - ContainerPort util.IntOrString `json:"containerPort,omitempty"` + // Optional: If unspecified, the service port is used (an identity map). + ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"` // Optional: Supports "ClientIP" and "None". Used to maintain session affinity. - SessionAffinity AffinityType `json:"sessionAffinity,omitempty"` + SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` } // Service is a named abstraction of software service (for example, mysql) consisting of local port @@ -759,71 +766,97 @@ type ServiceSpec struct { // will answer requests sent through the proxy. type Service struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the behavior of a service. - Spec ServiceSpec `json:"spec,omitempty"` + Spec ServiceSpec `json:"spec,omitempty" description:"specification of the desired behavior of the service; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` // Status represents the current status of a service. - Status ServiceStatus `json:"status,omitempty"` + Status ServiceStatus `json:"status,omitempty" description:"most recently observed status of the service; populated by the system, read-only; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // ServiceList holds a list of services. type ServiceList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - Items []Service `json:"items"` + Items []Service `json:"items" description:"list of services"` } // Endpoints is a collection of endpoints that implement the actual service, for example: // Name: "mysql", Endpoints: [{"ip": "10.10.1.1", "port": 1909}, {"ip": "10.10.2.2", "port": 8834}] type Endpoints struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Optional: The IP protocol for these endpoints. Supports "TCP" and // "UDP". Defaults to "TCP". - Protocol Protocol `json:"protocol,omitempty"` + Protocol Protocol `json:"protocol,omitempty" description:"IP protocol for endpoint ports; must be UDP or TCP; TCP if unspecified"` - Endpoints []Endpoint `json:"endpoints,omitempty"` + Endpoints []Endpoint `json:"endpoints,omitempty" description:"list of endpoints corresponding to a service"` } // Endpoint is a single IP endpoint of a service. type Endpoint struct { // Required: The IP of this endpoint. // TODO: This should allow hostname or IP, see #4447. - IP string `json:"ip"` + IP string `json:"ip" description:"IP of this endpoint"` // Required: The destination port to access. - Port int `json:"port"` + Port int `json:"port" description:"destination port of this endpoint"` + + // Optional: The kubernetes object related to the entry point. + TargetRef *ObjectReference `json:"targetRef,omitempty" description:"reference to object providing the endpoint"` } // EndpointsList is a list of endpoints. type EndpointsList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - Items []Endpoints `json:"items"` + Items []Endpoints `json:"items" description:"list of endpoints"` } // NodeSpec describes the attributes that a node is created with. type NodeSpec struct { - // Capacity represents the available resources of a node + // Capacity represents the available resources of a node. // see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md for more details. - Capacity ResourceList `json:"capacity,omitempty"` + Capacity ResourceList `json:"capacity,omitempty" description:"compute resource capacity of the node; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md"` // PodCIDR represents the pod IP range assigned to the node - PodCIDR string `json:"cidr,omitempty"` + PodCIDR string `json:"podCIDR,omitempty" description:"pod IP range assigned to the node"` + // External ID of the node assigned by some machine database (e.g. a cloud provider) + ExternalID string `json:"externalID,omitempty" description:"external ID assigned to the node by some machine database (e.g. a cloud provider)"` + // Unschedulable controls node schedulability of new pods. By default node is schedulable. + Unschedulable bool `json:"unschedulable,omitempty" description:"disable pod scheduling on the node"` +} + +// NodeSystemInfo is a set of ids/uuids to uniquely identify the node. +type NodeSystemInfo struct { + // MachineID is the machine-id reported by the node + MachineID string `json:"machineID"` + // SystemUUID is the system-uuid reported by the node + SystemUUID string `json:"systemUUID"` } // NodeStatus is information about the current status of a node. type NodeStatus struct { - // Queried from cloud provider, if available. - HostIP string `json:"hostIP,omitempty"` // NodePhase is the current lifecycle phase of the node. - Phase NodePhase `json:"phase,omitempty"` + Phase NodePhase `json:"phase,omitempty" description:"most recently observed lifecycle phase of the node"` // Conditions is an array of current node conditions. - Conditions []NodeCondition `json:"conditions,omitempty"` + Conditions []NodeCondition `json:"conditions,omitempty" description:"list of node conditions observed"` + // Queried from cloud provider, if available. + Addresses []NodeAddress `json:"addresses,omitempty" description:"list of addresses reachable to the node"` + // NodeSystemInfo is a set of ids/uuids to uniquely identify the node + NodeInfo NodeSystemInfo `json:"nodeInfo,omitempty"` +} + +// NodeInfo is the information collected on the node. +type NodeInfo struct { + TypeMeta `json:",inline"` + // Capacity represents the available resources of a node + Capacity ResourceList `json:"capacity,omitempty"` + // NodeSystemInfo is a set of ids/uuids to uniquely identify the node + NodeSystemInfo `json:",inline,omitempty"` } type NodePhase string @@ -838,25 +871,41 @@ const ( NodeTerminated NodePhase = "Terminated" ) -type NodeConditionKind string +type NodeConditionType string // These are valid conditions of node. Currently, we don't have enough information to decide // node condition. In the future, we will add more. The proposed set of conditions are: // NodeReachable, NodeLive, NodeReady, NodeSchedulable, NodeRunnable. const ( // NodeReachable means the node can be reached (in the sense of HTTP connection) from node controller. - NodeReachable NodeConditionKind = "Reachable" + NodeReachable NodeConditionType = "Reachable" // NodeReady means the node returns StatusOK for HTTP health check. - NodeReady NodeConditionKind = "Ready" + NodeReady NodeConditionType = "Ready" + // NodeSchedulable means the node is ready to accept new pods. + NodeSchedulable NodeConditionType = "Schedulable" ) type NodeCondition struct { - Kind NodeConditionKind `json:"kind"` - Status ConditionStatus `json:"status"` - LastProbeTime util.Time `json:"lastProbeTime,omitempty"` - LastTransitionTime util.Time `json:"lastTransitionTime,omitempty"` - Reason string `json:"reason,omitempty"` - Message string `json:"message,omitempty"` + Type NodeConditionType `json:"type" description:"type of node condition, one of Reachable, Ready"` + Status ConditionStatus `json:"status" description:"status of the condition, one of Full, None, Unknown"` + LastProbeTime util.Time `json:"lastProbeTime,omitempty" description:"last time the condition was probed"` + LastTransitionTime util.Time `json:"lastTransitionTime,omitempty" description:"last time the condition transit from one status to another"` + Reason string `json:"reason,omitempty" description:"(brief) reason for the condition's last transition"` + Message string `json:"message,omitempty" description:"human readable message indicating details about last transition"` +} + +type NodeAddressType string + +// These are valid address type of node. +const ( + NodeHostName NodeAddressType = "Hostname" + NodeExternalIP NodeAddressType = "ExternalIP" + NodeInternalIP NodeAddressType = "InternalIP" +) + +type NodeAddress struct { + Type NodeAddressType `json:"type"` + Address string `json:"address"` } // ResourceName is the name identifying various resources in a ResourceList. @@ -876,21 +925,21 @@ type ResourceList map[ResourceName]resource.Quantity // The name of the node according to etcd is in ID. type Node struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the behavior of a node. - Spec NodeSpec `json:"spec,omitempty"` + Spec NodeSpec `json:"spec,omitempty" description:"specification of a node; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` // Status describes the current status of a Node - Status NodeStatus `json:"status,omitempty"` + Status NodeStatus `json:"status,omitempty" description:"most recently observed status of the node; populated by the system, read-only; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // NodeList is a list of minions. type NodeList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - Items []Node `json:"items"` + Items []Node `json:"items" description:"list of nodes"` } // NamespaceSpec describes the attributes on a Namespace @@ -899,63 +948,83 @@ type NamespaceSpec struct { // NamespaceStatus is information about the current status of a Namespace. type NamespaceStatus struct { + // Phase is the current lifecycle phase of the namespace. + Phase NamespacePhase `json:"phase,omitempty" description:"phase is the current lifecycle phase of the namespace"` } +type NamespacePhase string + +// These are the valid phases of a namespace. +const ( + // NamespaceActive means the namespace is available for use in the system + NamespaceActive NamespacePhase = "Active" + // NamespaceTerminating means the namespace is undergoing graceful termination + NamespaceTerminating NamespacePhase = "Terminating" +) + // A namespace provides a scope for Names. // Use of multiple namespaces is optional type Namespace struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the behavior of the Namespace. - Spec NamespaceSpec `json:"spec,omitempty" description:"spec defines the behavior of the Namespace"` + Spec NamespaceSpec `json:"spec,omitempty" description:"spec defines the behavior of the Namespace; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` // Status describes the current status of a Namespace - Status NamespaceStatus `json:"status,omitempty" description:"status describes the current status of a Namespace"` + Status NamespaceStatus `json:"status,omitempty" description:"status describes the current status of a Namespace; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // NamespaceList is a list of Namespaces. type NamespaceList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Items is the list of Namespace objects in the list Items []Namespace `json:"items" description:"items is the list of Namespace objects in the list"` } -// Binding is written by a scheduler to cause a pod to be bound to a node. Name is not -// required for Bindings. +// Binding ties one object to another - for example, a pod is bound to a node by a scheduler. type Binding struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + TypeMeta `json:",inline"` + // ObjectMeta describes the object that is being bound. + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - // PodID is a Pod name to be bound to a node. - PodID string `json:"podID"` - // Host is the name of a node to bind to. - Host string `json:"host"` + // Target is the object to bind to. + Target ObjectReference `json:"target" description:"an object to bind to"` +} + +// DeleteOptions may be provided when deleting an API object +type DeleteOptions struct { + TypeMeta `json:",inline"` + + // Optional duration in seconds before the object should be deleted. Value must be non-negative integer. + // The value zero indicates delete immediately. If this value is nil, the default grace period for the + // specified type will be used. + GracePeriodSeconds *int64 `json:"gracePeriodSeconds" description:"the duration in seconds to wait before deleting this object; defaults to a per object value if not specified; zero means delete immediately"` } // Status is a return value for calls that don't return other objects. type Status struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // One of: "Success" or "Failure" - Status string `json:"status,omitempty"` + Status string `json:"status,omitempty" description:"status of the operation; either Success, or Failure"` // A human-readable description of the status of this operation. - Message string `json:"message,omitempty"` + Message string `json:"message,omitempty" description:"human-readable description of the status of this operation"` // A machine-readable description of why this operation is in the // "Failure" status. If this value is empty there // is no information available. A Reason clarifies an HTTP status // code but does not override it. - Reason StatusReason `json:"reason,omitempty"` + Reason StatusReason `json:"reason,omitempty" description:"machine-readable description of why this operation is in the 'Failure' status; if this value is empty there is no information available; a reason clarifies an HTTP status code but does not override it"` // Extended data associated with the reason. Each reason may define its // own extended details. This field is optional and the data returned // is not guaranteed to conform to any schema except that defined by // the reason type. - Details *StatusDetails `json:"details,omitempty"` + Details *StatusDetails `json:"details,omitempty" description:"extended data associated with the reason; each reason may define its own extended details; this field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type"` // Suggested HTTP return code for this status, 0 if not set. - Code int `json:"code,omitempty"` + Code int `json:"code,omitempty" description:"suggested HTTP return code for this status; 0 if not set"` } // StatusDetails is a set of additional properties that MAY be set by the @@ -967,13 +1036,13 @@ type Status struct { type StatusDetails struct { // The ID attribute of the resource associated with the status StatusReason // (when there is a single ID which can be described). - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty" description:"the ID attribute of the resource associated with the status StatusReason (when there is a single ID which can be described)"` // The kind attribute of the resource associated with the status StatusReason. // On some operations may differ from the requested resource Kind. - Kind string `json:"kind,omitempty"` + Kind string `json:"kind,omitempty" description:"the kind attribute of the resource associated with the status StatusReason; on some operations may differ from the requested resource Kind"` // The Causes array includes more details associated with the StatusReason // failure. Not all StatusReasons may provide detailed causes. - Causes []StatusCause `json:"causes,omitempty"` + Causes []StatusCause `json:"causes,omitempty" description:"the Causes array includes more details associated with the StatusReason failure; not all StatusReasons may provide detailed causes"` } // Values of Status.Status @@ -1048,10 +1117,10 @@ const ( type StatusCause struct { // A machine-readable description of the cause of the error. If this value is // empty there is no information available. - Type CauseType `json:"reason,omitempty"` + Type CauseType `json:"reason,omitempty" description:"machine-readable description of the cause of the error; if this value is empty there is no information available"` // A human-readable description of the cause of the error. This field may be // presented as-is to a reader. - Message string `json:"message,omitempty"` + Message string `json:"message,omitempty" description:"human-readable description of the cause of the error; this field may be presented as-is to a reader"` // The field of the resource that has caused this error, as named by its JSON // serialization. May include dot and postfix notation for nested attributes. // Arrays are zero-indexed. Fields may appear more than once in an array of @@ -1061,7 +1130,7 @@ type StatusCause struct { // Examples: // "name" - the field "name" on the current resource // "items[0].name" - the field "name" on the first array entry in "items" - Field string `json:"field,omitempty"` + Field string `json:"field,omitempty" description:"field of the resource that has caused this error, as named by its JSON serialization; may include dot and postfix notation for nested attributes; arrays are zero-indexed; fields may appear more than once in an array of causes due to fields having multiple errors"` } // CauseType is a machine readable value providing more detail about what @@ -1089,12 +1158,12 @@ const ( // ObjectReference contains enough information to let you inspect or modify the referred object. type ObjectReference struct { - Kind string `json:"kind,omitempty"` - Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` - UID types.UID `json:"uid,omitempty"` - APIVersion string `json:"apiVersion,omitempty"` - ResourceVersion string `json:"resourceVersion,omitempty"` + Kind string `json:"kind,omitempty" description:"kind of the referent"` + Namespace string `json:"namespace,omitempty" description:"namespace of the referent"` + Name string `json:"name,omitempty" description:"name of the referent"` + UID types.UID `json:"uid,omitempty" description:"uid of the referent"` + APIVersion string `json:"apiVersion,omitempty" description:"API version of the referent"` + ResourceVersion string `json:"resourceVersion,omitempty" description:"specific resourceVersion to which this reference is made, if any: https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#concurrency-control-and-consistency"` // Optional. If referring to a piece of an object instead of an entire object, this string // should contain information to identify the sub-object. For example, if the object @@ -1104,59 +1173,59 @@ type ObjectReference struct { // index 2 in this pod). This syntax is chosen only to have some well-defined way of // referencing a part of an object. // TODO: this design is not final and this field is subject to change in the future. - FieldPath string `json:"fieldPath,omitempty"` + FieldPath string `json:"fieldPath,omitempty" description:"if referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]"` } type EventSource struct { // Component from which the event is generated. - Component string `json:"component,omitempty"` + Component string `json:"component,omitempty" description:"component that generated the event"` // Host name on which the event is generated. - Host string `json:"host,omitempty"` + Host string `json:"host,omitempty" description:"name of the host where the event is generated"` } // Event is a report of an event somewhere in the cluster. // TODO: Decide whether to store these separately or with the object they apply to. type Event struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata"` + ObjectMeta `json:"metadata" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Required. The object that this event is about. - InvolvedObject ObjectReference `json:"involvedObject,omitempty"` + InvolvedObject ObjectReference `json:"involvedObject,omitempty" description:"object this event is about"` // Optional; this should be a short, machine understandable string that gives the reason // for this event being generated. // TODO: provide exact specification for format. - Reason string `json:"reason,omitempty"` + Reason string `json:"reason,omitempty" description:"short, machine understandable string that gives the reason for the transition into the object's current status"` // Optional. A human-readable description of the status of this operation. // TODO: decide on maximum length. - Message string `json:"message,omitempty"` + Message string `json:"message,omitempty" description:"human-readable description of the status of this operation"` // Optional. The component reporting this event. Should be a short machine understandable string. - Source EventSource `json:"source,omitempty"` + Source EventSource `json:"source,omitempty" description:"component reporting this event"` // The time at which the event was first recorded. (Time of server receipt is in TypeMeta.) - FirstTimestamp util.Time `json:"firstTimestamp,omitempty"` + FirstTimestamp util.Time `json:"firstTimestamp,omitempty" description:"the time at which the event was first recorded"` // The time at which the most recent occurance of this event was recorded. - LastTimestamp util.Time `json:"lastTimestamp,omitempty"` + LastTimestamp util.Time `json:"lastTimestamp,omitempty" description:"the time at which the most recent occurance of this event was recorded"` // The number of times this event has occurred. - Count int `json:"count,omitempty"` + Count int `json:"count,omitempty" description:"the number of times this event has occurred"` } // EventList is a list of events. type EventList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - Items []Event `json:"items"` + Items []Event `json:"items" description:"list of events"` } // List holds a list of objects, which may not be known by the server. type List struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` Items []runtime.RawExtension `json:"items" description:"list of objects"` } @@ -1190,16 +1259,16 @@ type LimitRangeSpec struct { // LimitRange sets resource usage limits for each kind of resource in a Namespace type LimitRange struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the limits enforced - Spec LimitRangeSpec `json:"spec,omitempty" description:"spec defines the limits enforced"` + Spec LimitRangeSpec `json:"spec,omitempty" description:"spec defines the limits enforced; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // LimitRangeList is a list of LimitRange items. type LimitRangeList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Items is a list of LimitRange objects Items []LimitRange `json:"items" description:"items is a list of LimitRange objects"` @@ -1234,29 +1303,19 @@ type ResourceQuotaStatus struct { // ResourceQuota sets aggregate quota restrictions enforced per namespace type ResourceQuota struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Spec defines the desired quota - Spec ResourceQuotaSpec `json:"spec,omitempty" description:"spec defines the desired quota"` - - // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` -} - -// ResourceQuotaUsage captures system observed quota status per namespace -// It is used to enforce atomic updates of a backing ResourceQuota.Status field in storage -type ResourceQuotaUsage struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + Spec ResourceQuotaSpec `json:"spec,omitempty" description:"spec defines the desired quota; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` + Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // ResourceQuotaList is a list of ResourceQuota items type ResourceQuotaList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Items is a list of ResourceQuota objects Items []ResourceQuota `json:"items" description:"items is a list of ResourceQuota objects"` @@ -1266,7 +1325,7 @@ type ResourceQuotaList struct { // the Data field must be less than MaxSecretSize bytes. type Secret struct { TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` // Data contains the secret data. Each key must be a valid DNS_SUBDOMAIN. // The serialized form of the secret data is a base64 encoded string, @@ -1287,7 +1346,7 @@ const ( type SecretList struct { TypeMeta `json:",inline"` - ListMeta `json:"metadata,omitempty"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` Items []Secret `json:"items" description:"items is a list of secret objects"` } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/events.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/events.go index 96f62d1b2fcf..8a305f14d6ba 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/events.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/events.go @@ -28,7 +28,7 @@ func ValidateEvent(event *api.Event) errs.ValidationErrorList { if event.Namespace != event.InvolvedObject.Namespace { allErrs = append(allErrs, errs.NewFieldInvalid("involvedObject.namespace", event.InvolvedObject.Namespace, "namespace does not match involvedObject")) } - if !util.IsDNSSubdomain(event.Namespace) { + if !util.IsDNS1123Subdomain(event.Namespace) { allErrs = append(allErrs, errs.NewFieldInvalid("namespace", event.Namespace, "")) } return allErrs diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/schema.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/schema.go index a4cbc0ebdd62..4bfaa7536e75 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/schema.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/schema.go @@ -126,7 +126,7 @@ func (s *SwaggerSchema) validateField(value interface{}, apiVersion, fieldName, if !ok { return NewInvalidTypeError(reflect.Array, reflect.TypeOf(value).Kind(), fieldName) } - arrType := *fieldDetails.Items[0].Ref + arrType := *fieldDetails.Items.Ref for ix := range arr { err := s.validateField(arr[ix], apiVersion, fmt.Sprintf("%s[%d]", fieldName, ix), arrType, nil) if err != nil { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/v1beta1-swagger.json b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/v1beta1-swagger.json index dfcc371bf6d4..7eb8663eb4eb 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/v1beta1-swagger.json +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/v1beta1-swagger.json @@ -943,11 +943,9 @@ "properties": { "command": { "type": "array", - "items": [ - { + "items": { "$ref": "string" } - ] }, "cpu": { "type": "integer", @@ -955,11 +953,9 @@ }, "env": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.EnvVar" } - ] }, "image": { "type": "string" @@ -982,11 +978,9 @@ }, "ports": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Port" } - ] }, "privileged": { "type": "boolean" @@ -996,11 +990,9 @@ }, "volumeMounts": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.VolumeMount" } - ] }, "workingDir": { "type": "string" @@ -1018,11 +1010,9 @@ "properties": { "containers": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Container" } - ] }, "id": { "type": "string" @@ -1038,11 +1028,9 @@ }, "volumes": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Volume" } - ] } } }, @@ -1075,11 +1063,9 @@ }, "endpoints": { "type": "array", - "items": [ - { + "items": { "$ref": "string" } - ] }, "id": { "type": "string" @@ -1130,11 +1116,9 @@ }, "items": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Endpoints" } - ] }, "kind": { "type": "string" @@ -1260,11 +1244,9 @@ }, "items": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Event" } - ] }, "kind": { "type": "string" @@ -1288,11 +1270,9 @@ "properties": { "command": { "type": "array", - "items": [ - { + "items": { "$ref": "string" } - ] } } }, @@ -1482,22 +1462,18 @@ }, "items": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Minion" } - ] }, "kind": { "type": "string" }, "minions": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Minion" } - ] }, "namespace": { "type": "string" @@ -1639,11 +1615,9 @@ }, "items": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Pod" } - ] }, "kind": { "type": "string" @@ -1813,11 +1787,9 @@ }, "items": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.ReplicationController" } - ] }, "kind": { "type": "string" @@ -1942,11 +1914,9 @@ }, "publicIPs": { "type": "array", - "items": [ - { + "items": { "$ref": "string" } - ] }, "resourceVersion": { "type": "uint64" @@ -1999,11 +1969,9 @@ }, "items": { "type": "array", - "items": [ - { + "items": { "$ref": "v1beta1.Service" } - ] }, "kind": { "type": "string" diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/validation.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/validation.go index 824799e8d8dc..4502005917ac 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/validation.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/validation.go @@ -18,6 +18,7 @@ package validation import ( "fmt" + "path" "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -30,7 +31,6 @@ import ( "github.com/golang/glog" ) -const qualifiedNameErrorMsg string = "must match regex [" + util.DNS1123SubdomainFmt + " / ] " + util.DNS1123LabelFmt const cIdentifierErrorMsg string = "must match regex " + util.CIdentifierFmt const isNegativeErrorMsg string = "value must not be negative" @@ -38,8 +38,10 @@ func intervalErrorMsg(lo, hi int) string { return fmt.Sprintf("must be greater than %d and less than %d", lo, hi) } +var labelValueErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.LabelValueMaxLength, util.LabelValueFmt) +var qualifiedNameErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.QualifiedNameMaxLength, util.QualifiedNameFmt) var dnsSubdomainErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.DNS1123SubdomainMaxLength, util.DNS1123SubdomainFmt) -var dnsLabelErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.DNS1123LabelMaxLength, util.DNS1123LabelFmt) +var dns1123LabelErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.DNS1123LabelMaxLength, util.DNS1123LabelFmt) var dns952LabelErrorMsg string = fmt.Sprintf("must have at most %d characters and match regex %s", util.DNS952LabelMaxLength, util.DNS952LabelFmt) var pdPartitionErrorMsg string = intervalErrorMsg(0, 255) var portRangeErrorMsg string = intervalErrorMsg(0, 65536) @@ -49,10 +51,13 @@ const totalAnnotationSizeLimitB int = 64 * (1 << 10) // 64 kB // ValidateLabels validates that a set of labels are correctly defined. func ValidateLabels(labels map[string]string, field string) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} - for k := range labels { + for k, v := range labels { if !util.IsQualifiedName(k) { allErrs = append(allErrs, errs.NewFieldInvalid(field, k, qualifiedNameErrorMsg)) } + if !util.IsValidLabelValue(v) { + allErrs = append(allErrs, errs.NewFieldInvalid(field, v, labelValueErrorMsg)) + } } return allErrs } @@ -60,6 +65,19 @@ func ValidateLabels(labels map[string]string, field string) errs.ValidationError // ValidateAnnotations validates that a set of annotations are correctly defined. func ValidateAnnotations(annotations map[string]string, field string) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} + var totalSize int64 + for k, v := range annotations { + if !util.IsQualifiedName(strings.ToLower(k)) { + allErrs = append(allErrs, errs.NewFieldInvalid(field, k, qualifiedNameErrorMsg)) + } + if !util.IsValidAnnotationValue(v) { + allErrs = append(allErrs, errs.NewFieldInvalid(field, k, "")) + } + totalSize += (int64)(len(k)) + (int64)(len(v)) + } + if totalSize > (int64)(totalAnnotationSizeLimitB) { + allErrs = append(allErrs, errs.NewFieldTooLong("annotations", "")) + } return allErrs } @@ -140,7 +158,7 @@ func nameIsDNSSubdomain(name string, prefix bool) (bool, string) { if prefix { name = maskTrailingDash(name) } - if util.IsDNSSubdomain(name) { + if util.IsDNS1123Subdomain(name) { return true, "" } return false, dnsSubdomainErrorMsg @@ -170,7 +188,7 @@ func ValidateObjectMeta(meta *api.ObjectMeta, requiresNamespace bool, nameFn Val // if the generated name validates, but the calculated value does not, it's a problem with generation, and we // report it here. This may confuse users, but indicates a programming bug and still must be validated. if len(meta.Name) == 0 { - allErrs = append(allErrs, errs.NewFieldRequired("name", meta.Name)) + allErrs = append(allErrs, errs.NewFieldRequired("name")) } else { if ok, qualifier := nameFn(meta.Name, false); !ok { allErrs = append(allErrs, errs.NewFieldInvalid("name", meta.Name, qualifier)) @@ -179,8 +197,8 @@ func ValidateObjectMeta(meta *api.ObjectMeta, requiresNamespace bool, nameFn Val if requiresNamespace { if len(meta.Namespace) == 0 { - allErrs = append(allErrs, errs.NewFieldRequired("namespace", meta.Namespace)) - } else if !util.IsDNSSubdomain(meta.Namespace) { + allErrs = append(allErrs, errs.NewFieldRequired("namespace")) + } else if !util.IsDNS1123Subdomain(meta.Namespace) { allErrs = append(allErrs, errs.NewFieldInvalid("namespace", meta.Namespace, dnsSubdomainErrorMsg)) } } else { @@ -202,7 +220,10 @@ func ValidateObjectMetaUpdate(old, meta *api.ObjectMeta) errs.ValidationErrorLis if len(meta.UID) == 0 { meta.UID = old.UID } - if meta.CreationTimestamp.IsZero() { + // ignore changes to timestamp + if old.CreationTimestamp.IsZero() { + old.CreationTimestamp = meta.CreationTimestamp + } else { meta.CreationTimestamp = old.CreationTimestamp } @@ -230,11 +251,11 @@ func validateVolumes(volumes []api.Volume) (util.StringSet, errs.ValidationError allNames := util.StringSet{} for i, vol := range volumes { - el := validateSource(&vol.Source).Prefix("source") + el := validateSource(&vol.VolumeSource).Prefix("source") if len(vol.Name) == 0 { - el = append(el, errs.NewFieldRequired("name", vol.Name)) - } else if !util.IsDNSLabel(vol.Name) { - el = append(el, errs.NewFieldInvalid("name", vol.Name, dnsLabelErrorMsg)) + el = append(el, errs.NewFieldRequired("name")) + } else if !util.IsDNS1123Label(vol.Name) { + el = append(el, errs.NewFieldInvalid("name", vol.Name, dns1123LabelErrorMsg)) } else if allNames.Has(vol.Name) { el = append(el, errs.NewFieldDuplicate("name", vol.Name)) } @@ -270,6 +291,10 @@ func validateSource(source *api.VolumeSource) errs.ValidationErrorList { numVolumes++ allErrs = append(allErrs, validateSecretVolumeSource(source.Secret).Prefix("secret")...) } + if source.NFS != nil { + numVolumes++ + allErrs = append(allErrs, validateNFS(source.NFS).Prefix("nfs")...) + } if numVolumes != 1 { allErrs = append(allErrs, errs.NewFieldInvalid("", source, "exactly 1 volume type is required")) } @@ -279,7 +304,7 @@ func validateSource(source *api.VolumeSource) errs.ValidationErrorList { func validateHostPathVolumeSource(hostDir *api.HostPathVolumeSource) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} if hostDir.Path == "" { - allErrs = append(allErrs, errs.NewFieldRequired("path", hostDir.Path)) + allErrs = append(allErrs, errs.NewFieldRequired("path")) } return allErrs } @@ -287,7 +312,7 @@ func validateHostPathVolumeSource(hostDir *api.HostPathVolumeSource) errs.Valida func validateGitRepoVolumeSource(gitRepo *api.GitRepoVolumeSource) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} if gitRepo.Repository == "" { - allErrs = append(allErrs, errs.NewFieldRequired("repository", gitRepo.Repository)) + allErrs = append(allErrs, errs.NewFieldRequired("repository")) } return allErrs } @@ -295,10 +320,10 @@ func validateGitRepoVolumeSource(gitRepo *api.GitRepoVolumeSource) errs.Validati func validateGCEPersistentDiskVolumeSource(PD *api.GCEPersistentDiskVolumeSource) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} if PD.PDName == "" { - allErrs = append(allErrs, errs.NewFieldRequired("pdName", PD.PDName)) + allErrs = append(allErrs, errs.NewFieldRequired("pdName")) } if PD.FSType == "" { - allErrs = append(allErrs, errs.NewFieldRequired("fsType", PD.FSType)) + allErrs = append(allErrs, errs.NewFieldRequired("fsType")) } if PD.Partition < 0 || PD.Partition > 255 { allErrs = append(allErrs, errs.NewFieldInvalid("partition", PD.Partition, pdPartitionErrorMsg)) @@ -309,10 +334,10 @@ func validateGCEPersistentDiskVolumeSource(PD *api.GCEPersistentDiskVolumeSource func validateSecretVolumeSource(secretSource *api.SecretVolumeSource) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} if secretSource.Target.Name == "" { - allErrs = append(allErrs, errs.NewFieldRequired("target.name", "")) + allErrs = append(allErrs, errs.NewFieldRequired("target.name")) } if secretSource.Target.Namespace == "" { - allErrs = append(allErrs, errs.NewFieldRequired("target.namespace", "")) + allErrs = append(allErrs, errs.NewFieldRequired("target.namespace")) } if secretSource.Target.Kind != "Secret" { allErrs = append(allErrs, errs.NewFieldInvalid("target.kind", secretSource.Target.Kind, "Secret")) @@ -320,17 +345,31 @@ func validateSecretVolumeSource(secretSource *api.SecretVolumeSource) errs.Valid return allErrs } +func validateNFS(nfs *api.NFSVolumeSource) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if nfs.Server == "" { + allErrs = append(allErrs, errs.NewFieldRequired("server")) + } + if nfs.Path == "" { + allErrs = append(allErrs, errs.NewFieldRequired("path")) + } + if !path.IsAbs(nfs.Path) { + allErrs = append(allErrs, errs.NewFieldInvalid("path", nfs.Path, "must be an absolute path")) + } + return allErrs +} + var supportedPortProtocols = util.NewStringSet(string(api.ProtocolTCP), string(api.ProtocolUDP)) -func validatePorts(ports []api.Port) errs.ValidationErrorList { +func validatePorts(ports []api.ContainerPort) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} allNames := util.StringSet{} for i, port := range ports { pErrs := errs.ValidationErrorList{} if len(port.Name) > 0 { - if len(port.Name) > util.DNS1123LabelMaxLength || !util.IsDNSLabel(port.Name) { - pErrs = append(pErrs, errs.NewFieldInvalid("name", port.Name, dnsLabelErrorMsg)) + if len(port.Name) > util.DNS1123LabelMaxLength || !util.IsDNS1123Label(port.Name) { + pErrs = append(pErrs, errs.NewFieldInvalid("name", port.Name, dns1123LabelErrorMsg)) } else if allNames.Has(port.Name) { pErrs = append(pErrs, errs.NewFieldDuplicate("name", port.Name)) } else { @@ -346,7 +385,7 @@ func validatePorts(ports []api.Port) errs.ValidationErrorList { pErrs = append(pErrs, errs.NewFieldInvalid("hostPort", port.HostPort, portRangeErrorMsg)) } if len(port.Protocol) == 0 { - pErrs = append(pErrs, errs.NewFieldRequired("protocol", port.Protocol)) + pErrs = append(pErrs, errs.NewFieldRequired("protocol")) } else if !supportedPortProtocols.Has(strings.ToUpper(string(port.Protocol))) { pErrs = append(pErrs, errs.NewFieldNotSupported("protocol", port.Protocol)) } @@ -361,7 +400,7 @@ func validateEnv(vars []api.EnvVar) errs.ValidationErrorList { for i, ev := range vars { vErrs := errs.ValidationErrorList{} if len(ev.Name) == 0 { - vErrs = append(vErrs, errs.NewFieldRequired("name", ev.Name)) + vErrs = append(vErrs, errs.NewFieldRequired("name")) } if !util.IsCIdentifier(ev.Name) { vErrs = append(vErrs, errs.NewFieldInvalid("name", ev.Name, cIdentifierErrorMsg)) @@ -377,12 +416,12 @@ func validateVolumeMounts(mounts []api.VolumeMount, volumes util.StringSet) errs for i, mnt := range mounts { mErrs := errs.ValidationErrorList{} if len(mnt.Name) == 0 { - mErrs = append(mErrs, errs.NewFieldRequired("name", mnt.Name)) + mErrs = append(mErrs, errs.NewFieldRequired("name")) } else if !volumes.Has(mnt.Name) { mErrs = append(mErrs, errs.NewFieldNotFound("name", mnt.Name)) } if len(mnt.MountPath) == 0 { - mErrs = append(mErrs, errs.NewFieldRequired("mountPath", mnt.MountPath)) + mErrs = append(mErrs, errs.NewFieldRequired("mountPath")) } allErrs = append(allErrs, mErrs.PrefixIndex(i)...) } @@ -407,7 +446,7 @@ func validateProbe(probe *api.Probe) errs.ValidationErrorList { // AccumulateUniquePorts runs an extraction function on each Port of each Container, // accumulating the results and returning an error if any ports conflict. -func AccumulateUniquePorts(containers []api.Container, accumulator map[int]bool, extract func(*api.Port) int) errs.ValidationErrorList { +func AccumulateUniquePorts(containers []api.Container, accumulator map[int]bool, extract func(*api.ContainerPort) int) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} for ci, ctr := range containers { @@ -432,13 +471,13 @@ func AccumulateUniquePorts(containers []api.Container, accumulator map[int]bool, // a slice of containers. func checkHostPortConflicts(containers []api.Container) errs.ValidationErrorList { allPorts := map[int]bool{} - return AccumulateUniquePorts(containers, allPorts, func(p *api.Port) int { return p.HostPort }) + return AccumulateUniquePorts(containers, allPorts, func(p *api.ContainerPort) int { return p.HostPort }) } func validateExecAction(exec *api.ExecAction) errs.ValidationErrorList { allErrors := errs.ValidationErrorList{} if len(exec.Command) == 0 { - allErrors = append(allErrors, errs.NewFieldRequired("command", exec.Command)) + allErrors = append(allErrors, errs.NewFieldRequired("command")) } return allErrors } @@ -446,15 +485,22 @@ func validateExecAction(exec *api.ExecAction) errs.ValidationErrorList { func validateHTTPGetAction(http *api.HTTPGetAction) errs.ValidationErrorList { allErrors := errs.ValidationErrorList{} if len(http.Path) == 0 { - allErrors = append(allErrors, errs.NewFieldRequired("path", http.Path)) + allErrors = append(allErrors, errs.NewFieldRequired("path")) + } + if http.Port.Kind == util.IntstrInt && !util.IsValidPortNum(http.Port.IntVal) { + allErrors = append(allErrors, errs.NewFieldInvalid("port", http.Port, portRangeErrorMsg)) + } else if http.Port.Kind == util.IntstrString && len(http.Port.StrVal) == 0 { + allErrors = append(allErrors, errs.NewFieldRequired("port")) } return allErrors } func validateTCPSocketAction(tcp *api.TCPSocketAction) errs.ValidationErrorList { allErrors := errs.ValidationErrorList{} - if tcp.Port.IntVal == 0 { - allErrors = append(allErrors, errs.NewFieldRequired("port", tcp.Port)) + if tcp.Port.Kind == util.IntstrInt && !util.IsValidPortNum(tcp.Port.IntVal) { + allErrors = append(allErrors, errs.NewFieldInvalid("port", tcp.Port, portRangeErrorMsg)) + } else if tcp.Port.Kind == util.IntstrString && len(tcp.Port.StrVal) == 0 { + allErrors = append(allErrors, errs.NewFieldRequired("port")) } return allErrors } @@ -498,7 +544,7 @@ func validatePullPolicy(ctr *api.Container) errs.ValidationErrorList { case api.PullAlways, api.PullIfNotPresent, api.PullNever: break case "": - allErrors = append(allErrors, errs.NewFieldRequired("", ctr.ImagePullPolicy)) + allErrors = append(allErrors, errs.NewFieldRequired("")) default: allErrors = append(allErrors, errs.NewFieldNotSupported("", ctr.ImagePullPolicy)) } @@ -509,14 +555,18 @@ func validatePullPolicy(ctr *api.Container) errs.ValidationErrorList { func validateContainers(containers []api.Container, volumes util.StringSet) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} + if len(containers) == 0 { + return append(allErrs, errs.NewFieldRequired("")) + } + allNames := util.StringSet{} for i, ctr := range containers { cErrs := errs.ValidationErrorList{} capabilities := capabilities.Get() if len(ctr.Name) == 0 { - cErrs = append(cErrs, errs.NewFieldRequired("name", ctr.Name)) - } else if !util.IsDNSLabel(ctr.Name) { - cErrs = append(cErrs, errs.NewFieldInvalid("name", ctr.Name, dnsLabelErrorMsg)) + cErrs = append(cErrs, errs.NewFieldRequired("name")) + } else if !util.IsDNS1123Label(ctr.Name) { + cErrs = append(cErrs, errs.NewFieldInvalid("name", ctr.Name, dns1123LabelErrorMsg)) } else if allNames.Has(ctr.Name) { cErrs = append(cErrs, errs.NewFieldDuplicate("name", ctr.Name)) } else if ctr.Privileged && !capabilities.AllowPrivileged { @@ -525,7 +575,7 @@ func validateContainers(containers []api.Container, volumes util.StringSet) errs allNames.Insert(ctr.Name) } if len(ctr.Image) == 0 { - cErrs = append(cErrs, errs.NewFieldRequired("image", ctr.Image)) + cErrs = append(cErrs, errs.NewFieldRequired("image")) } if ctr.Lifecycle != nil { cErrs = append(cErrs, validateLifecycle(ctr.Lifecycle).Prefix("lifecycle")...) @@ -560,7 +610,7 @@ func ValidateManifest(manifest *api.ContainerManifest) errs.ValidationErrorList allErrs := errs.ValidationErrorList{} if len(manifest.Version) == 0 { - allErrs = append(allErrs, errs.NewFieldRequired("version", manifest.Version)) + allErrs = append(allErrs, errs.NewFieldRequired("version")) } else if !supportedManifestVersions.Has(strings.ToLower(manifest.Version)) { allErrs = append(allErrs, errs.NewFieldNotSupported("version", manifest.Version)) } @@ -573,20 +623,16 @@ func ValidateManifest(manifest *api.ContainerManifest) errs.ValidationErrorList } func validateRestartPolicy(restartPolicy *api.RestartPolicy) errs.ValidationErrorList { - numPolicies := 0 allErrors := errs.ValidationErrorList{} - if restartPolicy.Always != nil { - numPolicies++ - } - if restartPolicy.OnFailure != nil { - numPolicies++ - } - if restartPolicy.Never != nil { - numPolicies++ - } - if numPolicies != 1 { - allErrors = append(allErrors, errs.NewFieldInvalid("", restartPolicy, "only 1 policy is allowed")) + switch *restartPolicy { + case api.RestartPolicyAlways, api.RestartPolicyOnFailure, api.RestartPolicyNever: + break + case "": + allErrors = append(allErrors, errs.NewFieldRequired("")) + default: + allErrors = append(allErrors, errs.NewFieldNotSupported("", restartPolicy)) } + return allErrors } @@ -596,7 +642,7 @@ func validateDNSPolicy(dnsPolicy *api.DNSPolicy) errs.ValidationErrorList { case api.DNSClusterFirst, api.DNSDefault: break case "": - allErrors = append(allErrors, errs.NewFieldRequired("", *dnsPolicy)) + allErrors = append(allErrors, errs.NewFieldRequired("")) default: allErrors = append(allErrors, errs.NewFieldNotSupported("", dnsPolicy)) } @@ -656,6 +702,24 @@ func ValidatePodUpdate(newPod, oldPod *api.Pod) errs.ValidationErrorList { return allErrs } +// ValidatePodStatusUpdate tests to see if the update is legal for an end user to make. newPod is updated with fields +// that cannot be changed. +func ValidatePodStatusUpdate(newPod, oldPod *api.Pod) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldPod.ObjectMeta, &newPod.ObjectMeta).Prefix("metadata")...) + + // TODO: allow change when bindings are properly decoupled from pods + if newPod.Status.Host != oldPod.Status.Host { + allErrs = append(allErrs, errs.NewFieldInvalid("status.host", newPod.Status.Host, "pod host cannot be changed directly")) + } + + // For status update we ignore changes to pod spec. + newPod.Spec = oldPod.Spec + + return allErrs +} + var supportedSessionAffinityType = util.NewStringSet(string(api.AffinityTypeClientIP), string(api.AffinityTypeNone)) // ValidateService tests if required fields in the service are set. @@ -667,17 +731,22 @@ func ValidateService(service *api.Service) errs.ValidationErrorList { allErrs = append(allErrs, errs.NewFieldInvalid("spec.port", service.Spec.Port, portRangeErrorMsg)) } if len(service.Spec.Protocol) == 0 { - allErrs = append(allErrs, errs.NewFieldRequired("spec.protocol", service.Spec.Protocol)) + allErrs = append(allErrs, errs.NewFieldRequired("spec.protocol")) } else if !supportedPortProtocols.Has(strings.ToUpper(string(service.Spec.Protocol))) { allErrs = append(allErrs, errs.NewFieldNotSupported("spec.protocol", service.Spec.Protocol)) } + if service.Spec.ContainerPort.Kind == util.IntstrInt && service.Spec.ContainerPort.IntVal != 0 && !util.IsValidPortNum(service.Spec.ContainerPort.IntVal) { + allErrs = append(allErrs, errs.NewFieldInvalid("spec.containerPort", service.Spec.Port, portRangeErrorMsg)) + } else if service.Spec.ContainerPort.Kind == util.IntstrString && len(service.Spec.ContainerPort.StrVal) == 0 { + allErrs = append(allErrs, errs.NewFieldRequired("spec.containerPort")) + } if service.Spec.Selector != nil { allErrs = append(allErrs, ValidateLabels(service.Spec.Selector, "spec.selector")...) } if service.Spec.SessionAffinity == "" { - allErrs = append(allErrs, errs.NewFieldRequired("spec.sessionAffinity", service.Spec.SessionAffinity)) + allErrs = append(allErrs, errs.NewFieldRequired("spec.sessionAffinity")) } else if !supportedSessionAffinityType.Has(string(service.Spec.SessionAffinity)) { allErrs = append(allErrs, errs.NewFieldNotSupported("spec.sessionAffinity", service.Spec.SessionAffinity)) } @@ -722,14 +791,14 @@ func ValidateReplicationControllerSpec(spec *api.ReplicationControllerSpec) errs selector := labels.Set(spec.Selector).AsSelector() if selector.Empty() { - allErrs = append(allErrs, errs.NewFieldRequired("selector", spec.Selector)) + allErrs = append(allErrs, errs.NewFieldRequired("selector")) } if spec.Replicas < 0 { allErrs = append(allErrs, errs.NewFieldInvalid("replicas", spec.Replicas, isNegativeErrorMsg)) } if spec.Template == nil { - allErrs = append(allErrs, errs.NewFieldRequired("template", spec.Template)) + allErrs = append(allErrs, errs.NewFieldRequired("template")) } else { labels := labels.Set(spec.Template.Labels) if !selector.Matches(labels) { @@ -737,10 +806,8 @@ func ValidateReplicationControllerSpec(spec *api.ReplicationControllerSpec) errs } allErrs = append(allErrs, ValidatePodTemplateSpec(spec.Template, spec.Replicas).Prefix("template")...) // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). - if spec.Template.Spec.RestartPolicy.Always == nil { - // TODO: should probably be Unsupported - // TODO: api.RestartPolicy should have a String() method for nicer printing - allErrs = append(allErrs, errs.NewFieldInvalid("template.restartPolicy", spec.Template.Spec.RestartPolicy, "must be Always")) + if spec.Template.Spec.RestartPolicy != api.RestartPolicyAlways { + allErrs = append(allErrs, errs.NewFieldNotSupported("template.restartPolicy", spec.Template.Spec.RestartPolicy)) } } return allErrs @@ -761,8 +828,8 @@ func ValidatePodTemplateSpec(spec *api.PodTemplateSpec, replicas int) errs.Valid func ValidateReadOnlyPersistentDisks(volumes []api.Volume) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} for _, vol := range volumes { - if vol.Source.GCEPersistentDisk != nil { - if vol.Source.GCEPersistentDisk.ReadOnly == false { + if vol.GCEPersistentDisk != nil { + if vol.GCEPersistentDisk.ReadOnly == false { allErrs = append(allErrs, errs.NewFieldInvalid("GCEPersistentDisk.ReadOnly", false, "ReadOnly must be true for replicated pods > 1, as GCE PD can only be mounted on multiple machines if it is read-only.")) } } @@ -770,26 +837,6 @@ func ValidateReadOnlyPersistentDisks(volumes []api.Volume) errs.ValidationErrorL return allErrs } -// ValidateBoundPod tests if required fields on a bound pod are set. -// TODO: to be removed. -func ValidateBoundPod(pod *api.BoundPod) errs.ValidationErrorList { - allErrs := errs.ValidationErrorList{} - if len(pod.Name) == 0 { - allErrs = append(allErrs, errs.NewFieldRequired("name", pod.Name)) - } else { - if ok, qualifier := nameIsDNSSubdomain(pod.Name, false); !ok { - allErrs = append(allErrs, errs.NewFieldInvalid("name", pod.Name, qualifier)) - } - } - if len(pod.Namespace) == 0 { - allErrs = append(allErrs, errs.NewFieldRequired("namespace", pod.Namespace)) - } else if !util.IsDNSSubdomain(pod.Namespace) { - allErrs = append(allErrs, errs.NewFieldInvalid("namespace", pod.Namespace, dnsSubdomainErrorMsg)) - } - allErrs = append(allErrs, ValidatePodSpec(&pod.Spec).Prefix("spec")...) - return allErrs -} - // ValidateMinion tests if required fields in the node are set. func ValidateMinion(node *api.Node) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} @@ -813,6 +860,8 @@ func ValidateMinionUpdate(oldMinion *api.Node, minion *api.Node) errs.Validation oldMinion.ObjectMeta = minion.ObjectMeta // Allow users to update capacity oldMinion.Spec.Capacity = minion.Spec.Capacity + // Allow users to unschedule node + oldMinion.Spec.Unschedulable = minion.Spec.Unschedulable // Clear status oldMinion.Status = minion.Status @@ -868,7 +917,7 @@ func ValidateSecret(secret *api.Secret) errs.ValidationErrorList { totalSize := 0 for key, value := range secret.Data { - if !util.IsDNSSubdomain(key) { + if !util.IsDNS1123Subdomain(key) { allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("data[%s]", key), key, cIdentifierErrorMsg)) } @@ -921,6 +970,36 @@ func ValidateResourceQuota(resourceQuota *api.ResourceQuota) errs.ValidationErro return allErrs } +// ValidateResourceQuotaUpdate tests to see if the update is legal for an end user to make. +// newResourceQuota is updated with fields that cannot be changed. +func ValidateResourceQuotaUpdate(newResourceQuota, oldResourceQuota *api.ResourceQuota) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldResourceQuota.ObjectMeta, &newResourceQuota.ObjectMeta).Prefix("metadata")...) + for k := range newResourceQuota.Spec.Hard { + allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + } + newResourceQuota.Status = oldResourceQuota.Status + return allErrs +} + +// ValidateResourceQuotaStatusUpdate tests to see if the status update is legal for an end user to make. +// newResourceQuota is updated with fields that cannot be changed. +func ValidateResourceQuotaStatusUpdate(newResourceQuota, oldResourceQuota *api.ResourceQuota) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldResourceQuota.ObjectMeta, &newResourceQuota.ObjectMeta).Prefix("metadata")...) + if newResourceQuota.ResourceVersion == "" { + allErrs = append(allErrs, fmt.Errorf("ResourceVersion must be specified")) + } + for k := range newResourceQuota.Status.Hard { + allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + } + for k := range newResourceQuota.Status.Used { + allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + } + newResourceQuota.Spec = oldResourceQuota.Spec + return allErrs +} + // ValidateNamespace tests if required fields are set. func ValidateNamespace(namespace *api.Namespace) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} @@ -944,3 +1023,15 @@ func ValidateNamespaceUpdate(oldNamespace *api.Namespace, namespace *api.Namespa } return allErrs } + +// ValidateNamespaceStatusUpdate tests to see if the update is legal for an end user to make. newNamespace is updated with fields +// that cannot be changed. +func ValidateNamespaceStatusUpdate(newNamespace, oldNamespace *api.Namespace) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldNamespace.ObjectMeta, &newNamespace.ObjectMeta).Prefix("metadata")...) + if newNamespace.Status.Phase != oldNamespace.Status.Phase { + allErrs = append(allErrs, errs.NewFieldInvalid("status.phase", newNamespace.Status.Phase, "namespace phase cannot be changed directly")) + } + newNamespace.Spec = oldNamespace.Spec + return allErrs +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/validation_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/validation_test.go index ca2ac42f8dd9..331f6f442937 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/validation_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation/validation_test.go @@ -19,12 +19,12 @@ package validation import ( "strings" "testing" + "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" ) @@ -53,6 +53,27 @@ func TestValidateObjectMetaCustomName(t *testing.T) { } } +func TestValidateObjectMetaUpdateIgnoresCreationTimestamp(t *testing.T) { + if errs := ValidateObjectMetaUpdate( + &api.ObjectMeta{Name: "test", CreationTimestamp: util.NewTime(time.Unix(10, 0))}, + &api.ObjectMeta{Name: "test"}, + ); len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if errs := ValidateObjectMetaUpdate( + &api.ObjectMeta{Name: "test"}, + &api.ObjectMeta{Name: "test", CreationTimestamp: util.NewTime(time.Unix(10, 0))}, + ); len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if errs := ValidateObjectMetaUpdate( + &api.ObjectMeta{Name: "test", CreationTimestamp: util.NewTime(time.Unix(11, 0))}, + &api.ObjectMeta{Name: "test", CreationTimestamp: util.NewTime(time.Unix(10, 0))}, + ); len(errs) != 0 { + t.Fatalf("unexpected errors: %v", errs) + } +} + // Ensure trailing slash is allowed in generate name func TestValidateObjectMetaTrimsTrailingSlash(t *testing.T) { errs := ValidateObjectMeta(&api.ObjectMeta{Name: "test", GenerateName: "foo-"}, false, nameIsDNSSubdomain) @@ -75,6 +96,8 @@ func TestValidateLabels(t *testing.T) { {"1-num.2-num/3-num": "bar"}, {"1234/5678": "bar"}, {"1.2.3.4/5678": "bar"}, + {"UpperCaseAreOK123": "bar"}, + {"goodvalue": "123_-.BaR"}, } for i := range successCases { errs := ValidateLabels(successCases[i], "field") @@ -83,15 +106,14 @@ func TestValidateLabels(t *testing.T) { } } - errorCases := []map[string]string{ - {"NoUppercase123": "bar"}, + labelNameErrorCases := []map[string]string{ {"nospecialchars^=@": "bar"}, {"cantendwithadash-": "bar"}, {"only/one/slash": "bar"}, {strings.Repeat("a", 254): "bar"}, } - for i := range errorCases { - errs := ValidateLabels(errorCases[i], "field") + for i := range labelNameErrorCases { + errs := ValidateLabels(labelNameErrorCases[i], "field") if len(errs) != 1 { t.Errorf("case[%d] expected failure", i) } else { @@ -101,6 +123,24 @@ func TestValidateLabels(t *testing.T) { } } } + + labelValueErrorCases := []map[string]string{ + {"toolongvalue": strings.Repeat("a", 64)}, + {"backslashesinvalue": "some\\bad\\value"}, + {"nocommasallowed": "bad,value"}, + {"strangecharsinvalue": "?#$notsogood"}, + } + for i := range labelValueErrorCases { + errs := ValidateLabels(labelValueErrorCases[i], "field") + if len(errs) != 1 { + t.Errorf("case[%d] expected failure", i) + } else { + detail := errs[0].(*errors.ValidationError).Detail + if detail != labelValueErrorMsg { + t.Errorf("error detail %s should be equal %s", detail, labelValueErrorMsg) + } + } + } } func TestValidateAnnotations(t *testing.T) { @@ -118,6 +158,11 @@ func TestValidateAnnotations(t *testing.T) { {"1234/5678": "bar"}, {"1.2.3.4/5678": "bar"}, {"UpperCase123": "bar"}, + {"a": strings.Repeat("b", (64*1024)-1)}, + { + "a": strings.Repeat("b", (32*1024)-1), + "c": strings.Repeat("d", (32*1024)-1), + }, } for i := range successCases { errs := ValidateAnnotations(successCases[i], "field") @@ -126,34 +171,46 @@ func TestValidateAnnotations(t *testing.T) { } } - errorCases := []map[string]string{ + nameErrorCases := []map[string]string{ {"nospecialchars^=@": "bar"}, {"cantendwithadash-": "bar"}, {"only/one/slash": "bar"}, {strings.Repeat("a", 254): "bar"}, } - for i := range errorCases { - errs := ValidateAnnotations(errorCases[i], "field") + for i := range nameErrorCases { + errs := ValidateAnnotations(nameErrorCases[i], "field") + if len(errs) != 1 { + t.Errorf("case[%d] expected failure", i) + } + detail := errs[0].(*errors.ValidationError).Detail + if detail != qualifiedNameErrorMsg { + t.Errorf("error detail %s should be equal %s", detail, qualifiedNameErrorMsg) + } + } + totalSizeErrorCases := []map[string]string{ + {"a": strings.Repeat("b", 64*1024)}, + { + "a": strings.Repeat("b", 32*1024), + "c": strings.Repeat("d", 32*1024), + }, + } + for i := range totalSizeErrorCases { + errs := ValidateAnnotations(totalSizeErrorCases[i], "field") if len(errs) != 1 { t.Errorf("case[%d] expected failure", i) - } else { - detail := errs[0].(*errors.ValidationError).Detail - if detail != qualifiedNameErrorMsg { - t.Errorf("error detail %s should be equal %s", detail, qualifiedNameErrorMsg) - } } } } func TestValidateVolumes(t *testing.T) { successCase := []api.Volume{ - {Name: "abc", Source: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path1"}}}, - {Name: "123", Source: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path2"}}}, - {Name: "abc-123", Source: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path3"}}}, - {Name: "empty", Source: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, - {Name: "gcepd", Source: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}}, - {Name: "gitrepo", Source: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{"my-repo", "hashstring"}}}, - {Name: "secret", Source: api.VolumeSource{Secret: &api.SecretVolumeSource{api.ObjectReference{Namespace: api.NamespaceDefault, Name: "my-secret", Kind: "Secret"}}}}, + {Name: "abc", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path1"}}}, + {Name: "123", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path2"}}}, + {Name: "abc-123", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/path3"}}}, + {Name: "empty", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}}, + {Name: "gitrepo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{"my-repo", "hashstring"}}}, + {Name: "secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{api.ObjectReference{Namespace: api.NamespaceDefault, Name: "my-secret", Kind: "Secret"}}}}, } names, errs := validateVolumes(successCase) if len(errs) != 0 { @@ -168,10 +225,10 @@ func TestValidateVolumes(t *testing.T) { T errors.ValidationErrorType F string }{ - "zero-length name": {[]api.Volume{{Name: "", Source: emptyVS}}, errors.ValidationErrorTypeRequired, "[0].name"}, - "name > 63 characters": {[]api.Volume{{Name: strings.Repeat("a", 64), Source: emptyVS}}, errors.ValidationErrorTypeInvalid, "[0].name"}, - "name not a DNS label": {[]api.Volume{{Name: "a.b.c", Source: emptyVS}}, errors.ValidationErrorTypeInvalid, "[0].name"}, - "name not unique": {[]api.Volume{{Name: "abc", Source: emptyVS}, {Name: "abc", Source: emptyVS}}, errors.ValidationErrorTypeDuplicate, "[1].name"}, + "zero-length name": {[]api.Volume{{Name: "", VolumeSource: emptyVS}}, errors.ValidationErrorTypeRequired, "[0].name"}, + "name > 63 characters": {[]api.Volume{{Name: strings.Repeat("a", 64), VolumeSource: emptyVS}}, errors.ValidationErrorTypeInvalid, "[0].name"}, + "name not a DNS label": {[]api.Volume{{Name: "a.b.c", VolumeSource: emptyVS}}, errors.ValidationErrorTypeInvalid, "[0].name"}, + "name not unique": {[]api.Volume{{Name: "abc", VolumeSource: emptyVS}, {Name: "abc", VolumeSource: emptyVS}}, errors.ValidationErrorTypeDuplicate, "[1].name"}, } for k, v := range errorCases { _, errs := validateVolumes(v.V) @@ -187,15 +244,15 @@ func TestValidateVolumes(t *testing.T) { t.Errorf("%s: expected errors to have field %s: %v", k, v.F, errs[i]) } detail := errs[i].(*errors.ValidationError).Detail - if detail != "" && detail != dnsLabelErrorMsg { - t.Errorf("%s: expected error detail either empty or %s, got %s", k, dnsLabelErrorMsg, detail) + if detail != "" && detail != dns1123LabelErrorMsg { + t.Errorf("%s: expected error detail either empty or %s, got %s", k, dns1123LabelErrorMsg, detail) } } } } func TestValidatePorts(t *testing.T) { - successCase := []api.Port{ + successCase := []api.ContainerPort{ {Name: "abc", ContainerPort: 80, HostPort: 80, Protocol: "TCP"}, {Name: "easy", ContainerPort: 82, Protocol: "TCP"}, {Name: "as", ContainerPort: 83, Protocol: "UDP"}, @@ -207,7 +264,7 @@ func TestValidatePorts(t *testing.T) { t.Errorf("expected success: %v", errs) } - nonCanonicalCase := []api.Port{ + nonCanonicalCase := []api.ContainerPort{ {ContainerPort: 80, Protocol: "TCP"}, } if errs := validatePorts(nonCanonicalCase); len(errs) != 0 { @@ -215,22 +272,22 @@ func TestValidatePorts(t *testing.T) { } errorCases := map[string]struct { - P []api.Port + P []api.ContainerPort T errors.ValidationErrorType F string D string }{ - "name > 63 characters": {[]api.Port{{Name: strings.Repeat("a", 64), ContainerPort: 80, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].name", dnsLabelErrorMsg}, - "name not a DNS label": {[]api.Port{{Name: "a.b.c", ContainerPort: 80, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].name", dnsLabelErrorMsg}, - "name not unique": {[]api.Port{ + "name > 63 characters": {[]api.ContainerPort{{Name: strings.Repeat("a", 64), ContainerPort: 80, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].name", dns1123LabelErrorMsg}, + "name not a DNS label": {[]api.ContainerPort{{Name: "a.b.c", ContainerPort: 80, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].name", dns1123LabelErrorMsg}, + "name not unique": {[]api.ContainerPort{ {Name: "abc", ContainerPort: 80, Protocol: "TCP"}, {Name: "abc", ContainerPort: 81, Protocol: "TCP"}, }, errors.ValidationErrorTypeDuplicate, "[1].name", ""}, - "zero container port": {[]api.Port{{ContainerPort: 0, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].containerPort", portRangeErrorMsg}, - "invalid container port": {[]api.Port{{ContainerPort: 65536, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].containerPort", portRangeErrorMsg}, - "invalid host port": {[]api.Port{{ContainerPort: 80, HostPort: 65536, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].hostPort", portRangeErrorMsg}, - "invalid protocol": {[]api.Port{{ContainerPort: 80, Protocol: "ICMP"}}, errors.ValidationErrorTypeNotSupported, "[0].protocol", ""}, - "protocol required": {[]api.Port{{Name: "abc", ContainerPort: 80}}, errors.ValidationErrorTypeRequired, "[0].protocol", ""}, + "zero container port": {[]api.ContainerPort{{ContainerPort: 0, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].containerPort", portRangeErrorMsg}, + "invalid container port": {[]api.ContainerPort{{ContainerPort: 65536, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].containerPort", portRangeErrorMsg}, + "invalid host port": {[]api.ContainerPort{{ContainerPort: 80, HostPort: 65536, Protocol: "TCP"}}, errors.ValidationErrorTypeInvalid, "[0].hostPort", portRangeErrorMsg}, + "invalid protocol": {[]api.ContainerPort{{ContainerPort: 80, Protocol: "ICMP"}}, errors.ValidationErrorTypeNotSupported, "[0].protocol", ""}, + "protocol required": {[]api.ContainerPort{{Name: "abc", ContainerPort: 80}}, errors.ValidationErrorTypeRequired, "[0].protocol", ""}, } for k, v := range errorCases { errs := validatePorts(v.P) @@ -331,6 +388,33 @@ func TestValidateProbe(t *testing.T) { } } +func TestValidateHandler(t *testing.T) { + successCases := []api.Handler{ + {Exec: &api.ExecAction{Command: []string{"echo"}}}, + {HTTPGet: &api.HTTPGetAction{Path: "/", Port: util.NewIntOrStringFromInt(1), Host: ""}}, + {HTTPGet: &api.HTTPGetAction{Path: "/foo", Port: util.NewIntOrStringFromInt(65535), Host: "host"}}, + {HTTPGet: &api.HTTPGetAction{Path: "/", Port: util.NewIntOrStringFromString("port"), Host: ""}}, + } + for _, h := range successCases { + if errs := validateHandler(&h); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + + errorCases := []api.Handler{ + {}, + {Exec: &api.ExecAction{Command: []string{}}}, + {HTTPGet: &api.HTTPGetAction{Path: "", Port: util.NewIntOrStringFromInt(0), Host: ""}}, + {HTTPGet: &api.HTTPGetAction{Path: "/foo", Port: util.NewIntOrStringFromInt(65536), Host: "host"}}, + {HTTPGet: &api.HTTPGetAction{Path: "", Port: util.NewIntOrStringFromString(""), Host: ""}}, + } + for _, h := range errorCases { + if errs := validateHandler(&h); len(errs) == 0 { + t.Errorf("expected failure for %#v", h) + } + } +} + func TestValidatePullPolicy(t *testing.T) { type T struct { Container api.Container @@ -433,9 +517,9 @@ func TestValidateContainers(t *testing.T) { }, "zero-length image": {{Name: "abc", Image: "", ImagePullPolicy: "IfNotPresent"}}, "host port not unique": { - {Name: "abc", Image: "image", Ports: []api.Port{{ContainerPort: 80, HostPort: 80, Protocol: "TCP"}}, + {Name: "abc", Image: "image", Ports: []api.ContainerPort{{ContainerPort: 80, HostPort: 80, Protocol: "TCP"}}, ImagePullPolicy: "IfNotPresent"}, - {Name: "def", Image: "image", Ports: []api.Port{{ContainerPort: 81, HostPort: 80, Protocol: "TCP"}}, + {Name: "def", Image: "image", Ports: []api.ContainerPort{{ContainerPort: 81, HostPort: 80, Protocol: "TCP"}}, ImagePullPolicy: "IfNotPresent"}, }, "invalid env var name": { @@ -469,6 +553,32 @@ func TestValidateContainers(t *testing.T) { ImagePullPolicy: "IfNotPresent", }, }, + "invalid lifecycle, no tcp socket port.": { + { + Name: "life-123", + Image: "image", + Lifecycle: &api.Lifecycle{ + PreStop: &api.Handler{ + TCPSocket: &api.TCPSocketAction{}, + }, + }, + ImagePullPolicy: "IfNotPresent", + }, + }, + "invalid lifecycle, zero tcp socket port.": { + { + Name: "life-123", + Image: "image", + Lifecycle: &api.Lifecycle{ + PreStop: &api.Handler{ + TCPSocket: &api.TCPSocketAction{ + Port: util.IntOrString{IntVal: 0}, + }, + }, + }, + ImagePullPolicy: "IfNotPresent", + }, + }, "invalid lifecycle, no action.": { { Name: "life-123", @@ -479,6 +589,28 @@ func TestValidateContainers(t *testing.T) { ImagePullPolicy: "IfNotPresent", }, }, + "invalid liveness probe, no tcp socket port.": { + { + Name: "life-123", + Image: "image", + LivenessProbe: &api.Probe{ + Handler: api.Handler{ + TCPSocket: &api.TCPSocketAction{}, + }, + }, + ImagePullPolicy: "IfNotPresent", + }, + }, + "invalid liveness probe, no action.": { + { + Name: "life-123", + Image: "image", + LivenessProbe: &api.Probe{ + Handler: api.Handler{}, + }, + ImagePullPolicy: "IfNotPresent", + }, + }, "privilege disabled": { {Name: "abc", Image: "image", Privileged: true}, }, @@ -524,9 +656,9 @@ func TestValidateContainers(t *testing.T) { func TestValidateRestartPolicy(t *testing.T) { successCases := []api.RestartPolicy{ - {Always: &api.RestartPolicyAlways{}}, - {OnFailure: &api.RestartPolicyOnFailure{}}, - {Never: &api.RestartPolicyNever{}}, + api.RestartPolicyAlways, + api.RestartPolicyOnFailure, + api.RestartPolicyNever, } for _, policy := range successCases { if errs := validateRestartPolicy(&policy); len(errs) != 0 { @@ -534,11 +666,8 @@ func TestValidateRestartPolicy(t *testing.T) { } } - errorCases := []api.RestartPolicy{ - {}, - {Always: &api.RestartPolicyAlways{}, Never: &api.RestartPolicyNever{}}, - {Never: &api.RestartPolicyNever{}, OnFailure: &api.RestartPolicyOnFailure{}}, - } + errorCases := []api.RestartPolicy{"", "newpolicy"} + for k, policy := range errorCases { if errs := validateRestartPolicy(&policy); len(errs) == 0 { t.Errorf("expected failure for %d", k) @@ -562,102 +691,20 @@ func TestValidateDNSPolicy(t *testing.T) { } } -func TestValidateManifest(t *testing.T) { - successCases := []api.ContainerManifest{ - {Version: "v1beta1", ID: "abc", RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst}, - {Version: "v1beta2", ID: "123", RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst}, - {Version: "V1BETA1", ID: "abc.123.do-re-mi", - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, DNSPolicy: api.DNSClusterFirst}, - { - Version: "v1beta1", - ID: "abc", - Volumes: []api.Volume{{Name: "vol1", Source: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/vol1"}}}, - {Name: "vol2", Source: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/mnt/vol2"}}}}, - Containers: []api.Container{ - { - Name: "abc", - Image: "image", - Command: []string{"foo", "bar"}, - WorkingDir: "/tmp", - Resources: api.ResourceRequirements{ - Limits: api.ResourceList{ - "cpu": resource.MustParse("1"), - "memory": resource.MustParse("1"), - }, - }, - Ports: []api.Port{ - {Name: "p1", ContainerPort: 80, HostPort: 8080, Protocol: "TCP"}, - {Name: "p2", ContainerPort: 81, Protocol: "TCP"}, - {ContainerPort: 82, Protocol: "TCP"}, - }, - Env: []api.EnvVar{ - {Name: "ev1", Value: "val1"}, - {Name: "ev2", Value: "val2"}, - {Name: "EV3", Value: "val3"}, - }, - VolumeMounts: []api.VolumeMount{ - {Name: "vol1", MountPath: "/foo"}, - {Name: "vol1", MountPath: "/bar"}, - }, - ImagePullPolicy: "IfNotPresent", - }, - }, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - } - for _, manifest := range successCases { - if errs := ValidateManifest(&manifest); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - } - - errorCases := map[string]api.ContainerManifest{ - "empty version": {Version: "", ID: "abc", - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst}, - "invalid version": {Version: "bogus", ID: "abc", - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst}, - "invalid volume name": { - Version: "v1beta1", - ID: "abc", - Volumes: []api.Volume{{Name: "vol.1", Source: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - "invalid container name": { - Version: "v1beta1", - ID: "abc", - Containers: []api.Container{{Name: "ctr.1", Image: "image", ImagePullPolicy: "IfNotPresent", - TerminationMessagePath: "/foo/bar"}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - } - for k, v := range errorCases { - if errs := ValidateManifest(&v); len(errs) == 0 { - t.Errorf("expected failure for %s", k) - } - } -} - func TestValidatePodSpec(t *testing.T) { successCases := []api.PodSpec{ { // Populate basic fields, leave defaults for most. - Volumes: []api.Volume{{Name: "vol", Source: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, + Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, { // Populate all fields. Volumes: []api.Volume{ - {Name: "vol", Source: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, }, Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, NodeSelector: map[string]string{ "key": "value", }, @@ -674,21 +721,28 @@ func TestValidatePodSpec(t *testing.T) { failureCases := map[string]api.PodSpec{ "bad volume": { Volumes: []api.Volume{{}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + "no containers": { + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, "bad container": { Containers: []api.Container{{}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, "bad DNS policy": { DNSPolicy: api.DNSPolicy("invalid"), - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, "bad restart policy": { - RestartPolicy: api.RestartPolicy{}, + RestartPolicy: "UnknowPolicy", DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, } for k, v := range failureCases { @@ -703,9 +757,9 @@ func TestValidatePod(t *testing.T) { { // Basic fields. ObjectMeta: api.ObjectMeta{Name: "123", Namespace: "ns"}, Spec: api.PodSpec{ - Volumes: []api.Volume{{Name: "vol", Source: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, + Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, }, @@ -713,10 +767,10 @@ func TestValidatePod(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "abc.123.do-re-mi", Namespace: "ns"}, Spec: api.PodSpec{ Volumes: []api.Volume{ - {Name: "vol", Source: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, }, Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, NodeSelector: map[string]string{ "key": "value", @@ -735,15 +789,17 @@ func TestValidatePod(t *testing.T) { "bad name": { ObjectMeta: api.ObjectMeta{Name: "", Namespace: "ns"}, Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, "bad namespace": { ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: ""}, Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, "bad spec": { @@ -761,27 +817,15 @@ func TestValidatePod(t *testing.T) { }, }, Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - "bad annotation": { - ObjectMeta: api.ObjectMeta{ - Name: "abc", - Namespace: "ns", - Annotations: map[string]string{ - "NoUppercaseOrSpecialCharsLike=Equals": "bar", - }, - }, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, } for k, v := range errorCases { if errs := ValidatePod(&v); len(errs) == 0 { - t.Errorf("expected failure for %s", k) + t.Errorf("expected failure for %q", k) } } } @@ -934,7 +978,7 @@ func TestValidatePodUpdate(t *testing.T) { Containers: []api.Container{ { Image: "foo:V1", - Ports: []api.Port{ + Ports: []api.ContainerPort{ {HostPort: 8080, ContainerPort: 80}, }, }, @@ -947,7 +991,7 @@ func TestValidatePodUpdate(t *testing.T) { Containers: []api.Container{ { Image: "foo:V2", - Ports: []api.Port{ + Ports: []api.ContainerPort{ {HostPort: 8000, ContainerPort: 80}, }, }, @@ -993,467 +1037,183 @@ func TestValidatePodUpdate(t *testing.T) { } } -func TestValidateBoundPods(t *testing.T) { - successCases := []api.BoundPod{ - { // Mostly empty. - ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "ns"}, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - { // Basic fields. - ObjectMeta: api.ObjectMeta{Name: "123", Namespace: "ns"}, - Spec: api.PodSpec{ - Volumes: []api.Volume{{Name: "vol", Source: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, - Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - { // Just about everything. - ObjectMeta: api.ObjectMeta{Name: "abc.123.do-re-mi", Namespace: "ns"}, - Spec: api.PodSpec{ - Volumes: []api.Volume{ - {Name: "vol", Source: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, - }, - Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - NodeSelector: map[string]string{ - "key": "value", - }, - Host: "foobar", - }, - }, - } - for _, pod := range successCases { - if errs := ValidateBoundPod(&pod); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - } - - errorCases := map[string]api.Pod{ - "zero-length name": { - ObjectMeta: api.ObjectMeta{Name: "", Namespace: "ns"}, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - "bad namespace": { - ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: ""}, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - "bad spec": { - ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "ns"}, - Spec: api.PodSpec{ - Containers: []api.Container{{Name: "name", ImagePullPolicy: "IfNotPresent"}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - "name > 253 characters": { - ObjectMeta: api.ObjectMeta{Name: strings.Repeat("a", 254), Namespace: "ns"}, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - "name not a DNS subdomain": { - ObjectMeta: api.ObjectMeta{Name: "a..b.c", Namespace: "ns"}, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - "name with underscore": { - ObjectMeta: api.ObjectMeta{Name: "a_b_c", Namespace: "ns"}, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, - DNSPolicy: api.DNSClusterFirst, - }, - }, - } - for k, v := range errorCases { - if errs := ValidatePod(&v); len(errs) != 1 { - t.Errorf("expected one failure for %s; got %d: %s", k, len(errs), errs) - } - } -} - func TestValidateService(t *testing.T) { testCases := []struct { - name string - svc api.Service - existing api.ServiceList - numErrs int + name string + makeSvc func(svc *api.Service) // given a basic valid service, each test case can customize it + numErrs int }{ { - name: "missing id", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "missing namespace", + makeSvc: func(s *api.Service) { + s.Namespace = "" }, - // Should fail because the ID is missing. numErrs: 1, }, { - name: "missing protocol", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - SessionAffinity: "None", - }, + name: "invalid namespace", + makeSvc: func(s *api.Service) { + s.Namespace = "-123" }, - // Should fail because protocol is missing. numErrs: 1, }, { - name: "missing session affinity", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - }, + name: "missing name", + makeSvc: func(s *api.Service) { + s.Name = "" }, - // Should fail because the session affinity is missing. numErrs: 1, }, { - name: "missing namespace", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "foo"}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "invalid name", + makeSvc: func(s *api.Service) { + s.Name = "-123" }, - // Should fail because the Namespace is missing. numErrs: 1, }, { - name: "invalid id", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "-123abc", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "too long name", + makeSvc: func(s *api.Service) { + s.Name = strings.Repeat("a", 25) }, - // Should fail because the ID is invalid. numErrs: 1, }, { - name: "invalid generate.base", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "valid", - GenerateName: "-123abc", - Namespace: api.NamespaceDefault, - }, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "invalid generateName", + makeSvc: func(s *api.Service) { + s.GenerateName = "-123" }, - // Should fail because the Base value for generation is invalid numErrs: 1, }, { - name: "invalid generateName", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "valid", - GenerateName: "abc1234567abc1234567abc1234567abc1234567abc1234567abc1234567", - Namespace: api.NamespaceDefault, - }, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "too long generateName", + makeSvc: func(s *api.Service) { + s.GenerateName = strings.Repeat("a", 25) }, - // Should fail because the generate name type is invalid. numErrs: 1, }, { - name: "missing port", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "invalid label", + makeSvc: func(s *api.Service) { + s.Labels["NoUppercaseOrSpecialCharsLike=Equals"] = "bar" }, - // Should fail because the port number is missing/invalid. numErrs: 1, }, { - name: "invalid port", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 66536, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "invalid annotation", + makeSvc: func(s *api.Service) { + s.Annotations["NoSpecialCharsLike=Equals"] = "bar" }, - // Should fail because the port number is invalid. numErrs: 1, }, { - name: "invalid protocol", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "INVALID", - SessionAffinity: "None", - }, + name: "invalid selector", + makeSvc: func(s *api.Service) { + s.Spec.Selector["NoSpecialCharsLike=Equals"] = "bar" }, - // Should fail because the protocol is invalid. numErrs: 1, }, { - name: "missing selector", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "missing session affinity", + makeSvc: func(s *api.Service) { + s.Spec.SessionAffinity = "" }, - // Should be ok because the selector is missing. - numErrs: 0, + numErrs: 1, }, { - name: "valid 1", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "missing protocol", + makeSvc: func(s *api.Service) { + s.Spec.Protocol = "" }, - numErrs: 0, + numErrs: 1, }, { - name: "valid 2", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "UDP", - SessionAffinity: "None", - }, + name: "invalid protocol", + makeSvc: func(s *api.Service) { + s.Spec.Protocol = "INVALID" }, - numErrs: 0, + numErrs: 1, }, { - name: "valid 3", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "UDP", - SessionAffinity: "None", - }, + name: "missing port", + makeSvc: func(s *api.Service) { + s.Spec.Port = 0 }, - numErrs: 0, + numErrs: 1, }, { - name: "external port in use", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 80, - CreateExternalLoadBalancer: true, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, - }, - existing: api.ServiceList{ - Items: []api.Service{ - { - ObjectMeta: api.ObjectMeta{Name: "def123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{Port: 80, CreateExternalLoadBalancer: true, Protocol: "TCP"}, - }, - }, + name: "invalid port", + makeSvc: func(s *api.Service) { + s.Spec.Port = 65536 }, - numErrs: 0, + numErrs: 1, }, { - name: "same port in use, but not external", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 80, - CreateExternalLoadBalancer: true, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, - }, - existing: api.ServiceList{ - Items: []api.Service{ - { - ObjectMeta: api.ObjectMeta{Name: "def123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{Port: 80, Protocol: "TCP"}, - }, - }, + name: "missing destinationPort string", + makeSvc: func(s *api.Service) { + s.Spec.ContainerPort = util.NewIntOrStringFromString("") }, - numErrs: 0, + numErrs: 1, }, { - name: "same port in use, but not external on input", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 80, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, - }, - existing: api.ServiceList{ - Items: []api.Service{ - { - ObjectMeta: api.ObjectMeta{Name: "def123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{Port: 80, CreateExternalLoadBalancer: true, Protocol: "TCP"}, - }, - }, + name: "invalid destinationPort int", + makeSvc: func(s *api.Service) { + s.Spec.ContainerPort = util.NewIntOrStringFromInt(65536) }, - numErrs: 0, + numErrs: 1, }, { - name: "same port in use, but neither external", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{Name: "abc123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 80, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, - }, - existing: api.ServiceList{ - Items: []api.Service{ - { - ObjectMeta: api.ObjectMeta{Name: "def123", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{Port: 80, Protocol: "TCP"}, - }, - }, + name: "nil selector", + makeSvc: func(s *api.Service) { + s.Spec.Selector = nil }, numErrs: 0, }, { - name: "invalid label", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "abc123", - Namespace: api.NamespaceDefault, - Labels: map[string]string{ - "NoUppercaseOrSpecialCharsLike=Equals": "bar", - }, - }, - Spec: api.ServiceSpec{ - Port: 8675, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "valid 1", + makeSvc: func(s *api.Service) { + // do nothing }, - numErrs: 1, + numErrs: 0, }, { - name: "invalid annotation", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "abc123", - Namespace: api.NamespaceDefault, - Annotations: map[string]string{ - "NoUppercaseOrSpecialCharsLike=Equals": "bar", - }, - }, - Spec: api.ServiceSpec{ - Port: 8675, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "valid 2", + makeSvc: func(s *api.Service) { + s.Spec.Protocol = "UDP" + s.Spec.ContainerPort = util.NewIntOrStringFromInt(12345) }, - numErrs: 1, + numErrs: 0, }, { - name: "invalid selector", - svc: api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "abc123", - Namespace: api.NamespaceDefault, - }, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar", "NoUppercaseOrSpecialCharsLike=Equals": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, + name: "valid 3", + makeSvc: func(s *api.Service) { + s.Spec.ContainerPort = util.NewIntOrStringFromString("http") }, - numErrs: 1, + numErrs: 0, }, } for _, tc := range testCases { - registry := registrytest.NewServiceRegistry() - registry.List = tc.existing - errs := ValidateService(&tc.svc) + svc := api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "valid", + Namespace: "valid", + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + Spec: api.ServiceSpec{ + Selector: map[string]string{"key": "val"}, + SessionAffinity: "None", + Port: 8675, + Protocol: "TCP", + }, + } + tc.makeSvc(&svc) + errs := ValidateService(&svc) if len(errs) != tc.numErrs { t.Errorf("Unexpected error list for case %q: %v", tc.name, utilerrors.NewAggregate(errs)) } } - - svc := api.Service{ - ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, - Spec: api.ServiceSpec{ - Port: 8675, - Selector: map[string]string{"foo": "bar"}, - Protocol: "TCP", - SessionAffinity: "None", - }, - } - errs := ValidateService(&svc) - if len(errs) != 0 { - t.Errorf("Unexpected non-zero error list: %#v", errs) - for i := range errs { - t.Errorf("Found error: %s", errs[i].Error()) - } - } } func TestValidateReplicationControllerUpdate(t *testing.T) { @@ -1464,8 +1224,9 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { Labels: validSelector, }, Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, } @@ -1475,9 +1236,10 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { Labels: validSelector, }, Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, - Volumes: []api.Volume{{Name: "gcepd", Source: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}}}, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}}}, }, }, } @@ -1485,7 +1247,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { invalidPodTemplate := api.PodTemplate{ Spec: api.PodTemplateSpec{ Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: api.ObjectMeta{ @@ -1624,8 +1386,9 @@ func TestValidateReplicationController(t *testing.T) { Labels: validSelector, }, Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, } @@ -1635,9 +1398,10 @@ func TestValidateReplicationController(t *testing.T) { Labels: validSelector, }, Spec: api.PodSpec{ - Volumes: []api.Volume{{Name: "gcepd", Source: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}}}, - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, } @@ -1645,7 +1409,7 @@ func TestValidateReplicationController(t *testing.T) { invalidPodTemplate := api.PodTemplate{ Spec: api.PodTemplateSpec{ Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{Always: &api.RestartPolicyAlways{}}, + RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: api.ObjectMeta{ @@ -1757,19 +1521,6 @@ func TestValidateReplicationController(t *testing.T) { Template: &invalidPodTemplate.Spec, }, }, - "invalid_annotation": { - ObjectMeta: api.ObjectMeta{ - Name: "abc-123", - Namespace: api.NamespaceDefault, - Annotations: map[string]string{ - "NoUppercaseOrSpecialCharsLike=Equals": "bar", - }, - }, - Spec: api.ReplicationControllerSpec{ - Selector: validSelector, - Template: &validPodTemplate.Spec, - }, - }, "invalid restart policy 1": { ObjectMeta: api.ObjectMeta{ Name: "abc-123", @@ -1779,10 +1530,9 @@ func TestValidateReplicationController(t *testing.T) { Selector: validSelector, Template: &api.PodTemplateSpec{ Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{ - OnFailure: &api.RestartPolicyOnFailure{}, - }, - DNSPolicy: api.DNSClusterFirst, + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, ObjectMeta: api.ObjectMeta{ Labels: validSelector, @@ -1799,10 +1549,9 @@ func TestValidateReplicationController(t *testing.T) { Selector: validSelector, Template: &api.PodTemplateSpec{ Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicy{ - Never: &api.RestartPolicyNever{}, - }, - DNSPolicy: api.DNSClusterFirst, + RestartPolicy: api.RestartPolicyNever, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, ObjectMeta: api.ObjectMeta{ Labels: validSelector, @@ -1844,7 +1593,9 @@ func TestValidateMinion(t *testing.T) { Labels: validSelector, }, Status: api.NodeStatus{ - HostIP: "something", + Addresses: []api.NodeAddress{ + {Type: api.NodeLegacyHostIP, Address: "something"}, + }, }, }, { @@ -1852,7 +1603,9 @@ func TestValidateMinion(t *testing.T) { Name: "abc", }, Status: api.NodeStatus{ - HostIP: "something", + Addresses: []api.NodeAddress{ + {Type: api.NodeLegacyHostIP, Address: "something"}, + }, }, }, } @@ -1869,7 +1622,7 @@ func TestValidateMinion(t *testing.T) { Labels: validSelector, }, Status: api.NodeStatus{ - HostIP: "something", + Addresses: []api.NodeAddress{}, }, }, "invalid-labels": { @@ -1878,12 +1631,6 @@ func TestValidateMinion(t *testing.T) { Labels: invalidSelector, }, }, - "invalid-annotations": { - ObjectMeta: api.ObjectMeta{ - Name: "abc-123", - Annotations: invalidSelector, - }, - }, } for k, v := range errorCases { errs := ValidateMinion(&v) @@ -1998,7 +1745,9 @@ func TestValidateMinionUpdate(t *testing.T) { Labels: map[string]string{"bar": "foo"}, }, Status: api.NodeStatus{ - HostIP: "1.2.3.4", + Addresses: []api.NodeAddress{ + {Type: api.NodeLegacyHostIP, Address: "1.2.3.4"}, + }, }, }, api.Node{ ObjectMeta: api.ObjectMeta{ @@ -2016,7 +1765,22 @@ func TestValidateMinionUpdate(t *testing.T) { Name: "foo", Labels: map[string]string{"Foo": "baz"}, }, - }, false}, + }, true}, + {api.Node{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Spec: api.NodeSpec{ + Unschedulable: false, + }, + }, api.Node{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Spec: api.NodeSpec{ + Unschedulable: true, + }, + }, true}, } for i, test := range tests { errs := ValidateMinionUpdate(&test.oldMinion, &test.minion) @@ -2185,7 +1949,7 @@ func TestValidateServiceUpdate(t *testing.T) { Name: "foo", Labels: map[string]string{"Foo": "baz"}, }, - }, false}, + }, true}, } for i, test := range tests { errs := ValidateServiceUpdate(&test.oldService, &test.service) @@ -2219,7 +1983,7 @@ func TestValidateResourceNames(t *testing.T) { {"my.favorite.app.co/_12345", false}, {"my.favorite.app.co/12345_", false}, {"kubernetes.io/..", false}, - {"kubernetes.io/" + longString, false}, + {"kubernetes.io/" + longString, true}, {"kubernetes.io//", false}, {"kubernetes.io", false}, {"kubernetes.io/will/not/work/", false}, @@ -2476,7 +2240,7 @@ func TestValidateNamespaceUpdate(t *testing.T) { Name: "foo", Labels: map[string]string{"Foo": "baz"}, }, - }, false}, + }, true}, } for i, test := range tests { errs := ValidateNamespaceUpdate(&test.oldNamespace, &test.namespace) diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/api_installer.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/api_installer.go index 48c6066838ee..0cf52dd58bb4 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/api_installer.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/api_installer.go @@ -22,9 +22,11 @@ import ( "net/url" gpath "path" "reflect" + "sort" "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -32,9 +34,9 @@ import ( ) type APIInstaller struct { - group *APIGroupVersion - prefix string // Path prefix where API resources are to be registered. - version string // The API version being installed. + group *APIGroupVersion + info *APIRequestInfoResolver + prefix string // Path prefix where API resources are to be registered. } // Struct capturing information about an action ("GET", "POST", "WATCH", PROXY", etc). @@ -46,7 +48,7 @@ type action struct { } // errEmptyName is returned when API requests do not fill the name section of the path. -var errEmptyName = fmt.Errorf("name must be provided") +var errEmptyName = errors.NewBadRequest("name must be provided") // Installs handlers for API resources. func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) { @@ -57,16 +59,24 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) { // Initialize the custom handlers. watchHandler := (&WatchHandler{ - storage: a.group.storage, - codec: a.group.codec, - linker: a.group.linker, - info: a.group.info, + storage: a.group.Storage, + codec: a.group.Codec, + linker: a.group.Linker, + info: a.info, }) - redirectHandler := (&RedirectHandler{a.group.storage, a.group.codec, a.group.context, a.group.info}) - proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.storage, a.group.codec, a.group.context, a.group.info}) - - for path, storage := range a.group.storage { - if err := a.registerResourceHandlers(path, storage, ws, watchHandler, redirectHandler, proxyHandler); err != nil { + redirectHandler := (&RedirectHandler{a.group.Storage, a.group.Codec, a.group.Context, a.info}) + proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info}) + + // Register the paths in a deterministic (sorted) order to get a deterministic swagger spec. + paths := make([]string, len(a.group.Storage)) + var i int = 0 + for path := range a.group.Storage { + paths[i] = path + i++ + } + sort.Strings(paths) + for _, path := range paths { + if err := a.registerResourceHandlers(path, a.group.Storage[path], ws, watchHandler, redirectHandler, proxyHandler); err != nil { errors = append(errors, err) } } @@ -76,27 +86,35 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) { func (a *APIInstaller) newWebService() *restful.WebService { ws := new(restful.WebService) ws.Path(a.prefix) - ws.Doc("API at " + a.prefix + " version " + a.version) + ws.Doc("API at " + a.prefix + " version " + a.group.Version) // TODO: change to restful.MIME_JSON when we set content type in client ws.Consumes("*/*") ws.Produces(restful.MIME_JSON) - ws.ApiVersion(a.version) + ws.ApiVersion(a.group.Version) return ws } func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage, ws *restful.WebService, watchHandler, redirectHandler, proxyHandler http.Handler) error { - codec := a.group.codec - admit := a.group.admit - context := a.group.context - resource := path + admit := a.group.Admit + context := a.group.Context + + var resource, subresource string + switch parts := strings.Split(path, "/"); len(parts) { + case 2: + resource, subresource = parts[0], parts[1] + case 1: + resource = parts[0] + default: + // TODO: support deeper paths + return fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)") + } object := storage.New() - // TODO: add scheme to APIInstaller rather than using api.Scheme - _, kind, err := api.Scheme.ObjectVersionAndKind(object) + _, kind, err := a.group.Typer.ObjectVersionAndKind(object) if err != nil { return err } - versionedPtr, err := api.Scheme.New(a.version, kind) + versionedPtr, err := a.group.Creater.New(a.group.Version, kind) if err != nil { return err } @@ -105,15 +123,15 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage var versionedList interface{} if lister, ok := storage.(RESTLister); ok { list := lister.NewList() - _, listKind, err := api.Scheme.ObjectVersionAndKind(list) - versionedListPtr, err := api.Scheme.New(a.version, listKind) + _, listKind, err := a.group.Typer.ObjectVersionAndKind(list) + versionedListPtr, err := a.group.Creater.New(a.group.Version, listKind) if err != nil { return err } versionedList = indirectArbitraryPointer(versionedListPtr) } - mapping, err := a.group.mapper.RESTMapping(kind, a.version) + mapping, err := a.group.Mapper.RESTMapping(kind, a.group.Version) if err != nil { return err } @@ -123,10 +141,25 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage lister, isLister := storage.(RESTLister) getter, isGetter := storage.(RESTGetter) deleter, isDeleter := storage.(RESTDeleter) + gracefulDeleter, isGracefulDeleter := storage.(RESTGracefulDeleter) updater, isUpdater := storage.(RESTUpdater) + patcher, isPatcher := storage.(RESTPatcher) _, isWatcher := storage.(ResourceWatcher) _, isRedirector := storage.(Redirector) + var versionedDeleterObject runtime.Object + switch { + case isGracefulDeleter: + object, err := a.group.Creater.New(a.group.Version, "DeleteOptions") + if err != nil { + return err + } + versionedDeleterObject = object + isDeleter = true + case isDeleter: + gracefulDeleter = GracefulDeleteAdapter{deleter} + } + var ctxFn ContextFunc ctxFn = func(req *restful.Request) api.Context { if ctx, ok := context.Get(req.Request); ok { @@ -143,51 +176,63 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage // Get the list of actions for the given scope. if scope.Name() != meta.RESTScopeNameNamespace { - itemPath := path + "/{name}" + resourcePath := resource + itemPath := resourcePath + "/{name}" + if len(subresource) > 0 { + itemPath = itemPath + "/" + subresource + resourcePath = itemPath + } nameParams := append(params, nameParam) - namer := rootScopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath)} + namer := rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath)} // Handler for standard REST verbs (GET, PUT, POST and DELETE). - actions = appendIf(actions, action{"LIST", path, params, namer}, isLister) - actions = appendIf(actions, action{"POST", path, params, namer}, isCreater) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params, namer}, allowWatchList) + actions = appendIf(actions, action{"LIST", resourcePath, params, namer}, isLister) + actions = appendIf(actions, action{"POST", resourcePath, params, namer}, isCreater) + actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, params, namer}, allowWatchList) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater) + actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer}, isPatcher) actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, isWatcher) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher) + actions = appendIf(actions, action{"REDIRECT", "redirect/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector) } else { // v1beta3 format with namespace in path if scope.ParamPath() { // Handler for standard REST verbs (GET, PUT, POST and DELETE). namespaceParam := ws.PathParameter(scope.ParamName(), scope.ParamDescription()).DataType("string") - namespacedPath := scope.ParamName() + "/{" + scope.ParamName() + "}/" + path + namespacedPath := scope.ParamName() + "/{" + scope.ParamName() + "}/" + resource namespaceParams := []*restful.Parameter{namespaceParam} + resourcePath := namespacedPath itemPath := namespacedPath + "/{name}" + if len(subresource) > 0 { + itemPath = itemPath + "/" + subresource + resourcePath = itemPath + } nameParams := append(namespaceParams, nameParam) - namer := scopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath), false} + namer := scopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath), false} - actions = appendIf(actions, action{"LIST", namespacedPath, namespaceParams, namer}, isLister) - actions = appendIf(actions, action{"POST", namespacedPath, namespaceParams, namer}, isCreater) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + namespacedPath, namespaceParams, namer}, allowWatchList) + actions = appendIf(actions, action{"LIST", resourcePath, namespaceParams, namer}, isLister) + actions = appendIf(actions, action{"POST", resourcePath, namespaceParams, namer}, isCreater) + actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, namespaceParams, namer}, allowWatchList) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater) + actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer}, isPatcher) actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, isWatcher) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher) + actions = appendIf(actions, action{"REDIRECT", "redirect/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector) // list across namespace. - namer = scopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath), true} - actions = appendIf(actions, action{"LIST", path, params, namer}, isLister) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, params, namer}, allowWatchList) + namer = scopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath), true} + actions = appendIf(actions, action{"LIST", resource, params, namer}, isLister) + actions = appendIf(actions, action{"WATCHLIST", "watch/" + resource, params, namer}, allowWatchList) } else { // Handler for standard REST verbs (GET, PUT, POST and DELETE). @@ -195,21 +240,28 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage namespaceParam := ws.QueryParameter(scope.ParamName(), scope.ParamDescription()).DataType("string") namespaceParams := []*restful.Parameter{namespaceParam} - itemPath := path + "/{name}" + basePath := resource + resourcePath := basePath + itemPath := resourcePath + "/{name}" + if len(subresource) > 0 { + itemPath = itemPath + "/" + subresource + resourcePath = itemPath + } nameParams := append(namespaceParams, nameParam) - namer := legacyScopeNaming{scope, a.group.linker, gpath.Join(a.prefix, itemPath)} + namer := legacyScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath)} - actions = appendIf(actions, action{"LIST", path, namespaceParams, namer}, isLister) - actions = appendIf(actions, action{"POST", path, namespaceParams, namer}, isCreater) - actions = appendIf(actions, action{"WATCHLIST", "/watch/" + path, namespaceParams, namer}, allowWatchList) + actions = appendIf(actions, action{"LIST", resourcePath, namespaceParams, namer}, isLister) + actions = appendIf(actions, action{"POST", resourcePath, namespaceParams, namer}, isCreater) + actions = appendIf(actions, action{"WATCHLIST", "watch/" + resourcePath, namespaceParams, namer}, allowWatchList) actions = appendIf(actions, action{"GET", itemPath, nameParams, namer}, isGetter) actions = appendIf(actions, action{"PUT", itemPath, nameParams, namer}, isUpdater) + actions = appendIf(actions, action{"PATCH", itemPath, nameParams, namer}, isPatcher) actions = appendIf(actions, action{"DELETE", itemPath, nameParams, namer}, isDeleter) - actions = appendIf(actions, action{"WATCH", "/watch/" + itemPath, nameParams, namer}, isWatcher) - actions = appendIf(actions, action{"REDIRECT", "/redirect/" + itemPath, nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) - actions = appendIf(actions, action{"PROXY", "/proxy/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"WATCH", "watch/" + itemPath, nameParams, namer}, isWatcher) + actions = appendIf(actions, action{"REDIRECT", "redirect/" + itemPath, nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath + "/{path:*}", nameParams, namer}, isRedirector) + actions = appendIf(actions, action{"PROXY", "proxy/" + itemPath, nameParams, namer}, isRedirector) } } @@ -234,7 +286,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage m := monitorFilter(action.Verb, resource) switch action.Verb { case "GET": // Get a resource. - route := ws.GET(action.Path).To(GetResource(getter, ctxFn, action.Namer, codec)). + route := ws.GET(action.Path).To(GetResource(getter, ctxFn, action.Namer, mapping.Codec)). Filter(m). Doc("read the specified " + kind). Operation("read" + kind). @@ -242,7 +294,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage addParams(route, action.Params) ws.Route(route) case "LIST": // List all resources of a kind. - route := ws.GET(action.Path).To(ListResource(lister, ctxFn, action.Namer, codec)). + route := ws.GET(action.Path).To(ListResource(lister, ctxFn, action.Namer, mapping.Codec, a.group.Version, resource)). Filter(m). Doc("list objects of kind " + kind). Operation("list" + kind). @@ -250,15 +302,25 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage addParams(route, action.Params) ws.Route(route) case "PUT": // Update a resource. - route := ws.PUT(action.Path).To(UpdateResource(updater, ctxFn, action.Namer, codec, resource, admit)). + route := ws.PUT(action.Path).To(UpdateResource(updater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)). + Filter(m). + Doc("replace the specified " + kind). + Operation("replace" + kind). + Reads(versionedObject) + addParams(route, action.Params) + ws.Route(route) + case "PATCH": // Partially update a resource + route := ws.PATCH(action.Path).To(PatchResource(patcher, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)). Filter(m). - Doc("update the specified " + kind). - Operation("update" + kind). + Doc("partially update the specified " + kind). + // TODO: toggle patch strategy by content type + // Consumes("application/merge-patch+json", "application/json-patch+json"). + Operation("patch" + kind). Reads(versionedObject) addParams(route, action.Params) ws.Route(route) case "POST": // Create a resource. - route := ws.POST(action.Path).To(CreateResource(creater, ctxFn, action.Namer, codec, resource, admit)). + route := ws.POST(action.Path).To(CreateResource(creater, ctxFn, action.Namer, mapping.Codec, a.group.Typer, resource, admit)). Filter(m). Doc("create a " + kind). Operation("create" + kind). @@ -266,10 +328,13 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage RESTStorage addParams(route, action.Params) ws.Route(route) case "DELETE": // Delete a resource. - route := ws.DELETE(action.Path).To(DeleteResource(deleter, ctxFn, action.Namer, codec, resource, kind, admit)). + route := ws.DELETE(action.Path).To(DeleteResource(gracefulDeleter, isGracefulDeleter, ctxFn, action.Namer, mapping.Codec, resource, kind, admit)). Filter(m). Doc("delete a " + kind). Operation("delete" + kind) + if isGracefulDeleter { + route.Reads(versionedDeleterObject) + } addParams(route, action.Params) ws.Route(route) case "WATCH": // Watch a resource. diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/apiserver.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/apiserver.go index 2307c0ae4ad3..c8f749133f9e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/apiserver.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/apiserver.go @@ -29,7 +29,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -52,10 +51,12 @@ var ( }, []string{"handler", "verb", "resource", "code"}, ) - requestLatencies = prometheus.NewSummaryVec( - prometheus.SummaryOpts{ + requestLatencies = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ Name: "apiserver_request_latencies", - Help: "Response latency summary in microseconds for each request handler and verb.", + Help: "Response latency distribution in microseconds for each request handler and verb.", + // Use buckets ranging from 125 ms to 8 seconds. + Buckets: prometheus.ExponentialBuckets(125000, 2.0, 7), }, []string{"handler", "verb"}, ) @@ -68,9 +69,9 @@ func init() { // monitor is a helper function for each HTTP request handler to use for // instrumenting basic request counter and latency metrics. -func monitor(handler, verb, resource string, httpCode int, reqStart time.Time) { - requestCounter.WithLabelValues(handler, verb, resource, strconv.Itoa(httpCode)).Inc() - requestLatencies.WithLabelValues(handler, verb).Observe(float64((time.Since(reqStart)) / time.Microsecond)) +func monitor(handler string, verb, resource *string, httpCode *int, reqStart time.Time) { + requestCounter.WithLabelValues(handler, *verb, *resource, strconv.Itoa(*httpCode)).Inc() + requestLatencies.WithLabelValues(handler, *verb).Observe(float64((time.Since(reqStart)) / time.Microsecond)) } // monitorFilter creates a filter that reports the metrics for a given resource and action. @@ -78,7 +79,8 @@ func monitorFilter(action, resource string) restful.FilterFunction { return func(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { reqStart := time.Now() chain.ProcessFilter(req, res) - monitor("rest", action, resource, res.StatusCode(), reqStart) + httpCode := res.StatusCode() + monitor("rest", &action, &resource, &httpCode, reqStart) } } @@ -88,72 +90,38 @@ type Mux interface { HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) } -// defaultAPIServer exposes nested objects for testability. -type defaultAPIServer struct { - http.Handler - group *APIGroupVersion -} - -// Handle returns a Handler function that exposes the provided storage interfaces -// as RESTful resources at prefix, serialized by codec, and also includes the support -// http resources. -// Note: This method is used only in tests. -func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, linker runtime.SelfLinker, admissionControl admission.Interface, contextMapper api.RequestContextMapper, mapper meta.RESTMapper) http.Handler { - prefix := path.Join(root, version) - group := NewAPIGroupVersion(storage, codec, root, prefix, linker, admissionControl, contextMapper, mapper) - container := restful.NewContainer() - container.Router(restful.CurlyRouter{}) - mux := container.ServeMux - group.InstallREST(container, root, version) - ws := new(restful.WebService) - InstallSupport(mux, ws) - container.Add(ws) - return &defaultAPIServer{mux, group} -} - // APIGroupVersion is a helper for exposing RESTStorage objects as http.Handlers via go-restful // It handles URLs of the form: // /${storage_key}[/${object_name}] // Where 'storage_key' points to a RESTStorage object stored in storage. type APIGroupVersion struct { - storage map[string]RESTStorage - codec runtime.Codec - prefix string - linker runtime.SelfLinker - admit admission.Interface - context api.RequestContextMapper - mapper meta.RESTMapper - // TODO: put me into a cleaner interface - info *APIRequestInfoResolver -} + Storage map[string]RESTStorage -// NewAPIGroupVersion returns an object that will serve a set of REST resources and their -// associated operations. The provided codec controls serialization and deserialization. -// This is a helper method for registering multiple sets of REST handlers under different -// prefixes onto a server. -// TODO: add multitype codec serialization -func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, root, prefix string, linker runtime.SelfLinker, admissionControl admission.Interface, contextMapper api.RequestContextMapper, mapper meta.RESTMapper) *APIGroupVersion { - return &APIGroupVersion{ - storage: storage, - codec: codec, - prefix: prefix, - linker: linker, - admit: admissionControl, - context: contextMapper, - mapper: mapper, - info: &APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(root, "/")), latest.RESTMapper}, - } + Root string + Version string + + Mapper meta.RESTMapper + + Codec runtime.Codec + Typer runtime.ObjectTyper + Creater runtime.ObjectCreater + Linker runtime.SelfLinker + + Admit admission.Interface + Context api.RequestContextMapper } // InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container. // It is expected that the provided path root prefix will serve all operations. Root MUST NOT end // in a slash. A restful WebService is created for the group and version. -func (g *APIGroupVersion) InstallREST(container *restful.Container, root string, version string) error { - prefix := path.Join(root, version) +func (g *APIGroupVersion) InstallREST(container *restful.Container) error { + info := &APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(g.Root, "/")), g.Mapper} + + prefix := path.Join(g.Root, g.Version) installer := &APIInstaller{ - group: g, - prefix: prefix, - version: version, + group: g, + info: info, + prefix: prefix, } ws, registrationErrors := installer.Install() container.Add(ws) @@ -290,7 +258,8 @@ func parseTimeout(str string) time.Duration { } glog.Errorf("Failed to parse %q: %v", str, err) } - return 30 * time.Second + // TODO: change back to 30s once #5180 is fixed + return 2 * time.Minute } func readBody(req *http.Request) ([]byte, error) { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/apiserver_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/apiserver_test.go index b94c3753334e..1a9718c5de55 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/apiserver_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/apiserver_test.go @@ -34,6 +34,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" apierrs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -41,6 +42,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny" + + "github.com/emicklei/go-restful" ) func convert(obj runtime.Object) (runtime.Object, error) { @@ -94,8 +97,7 @@ func init() { &api.Status{}) // "version" version // TODO: Use versioned api objects? - api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, - &api.Status{}) + api.Scheme.AddKnownTypes(testVersion, &Simple{}, &SimpleList{}, &api.DeleteOptions{}, &api.Status{}) nsMapper := newMapper() legacyNsMapper := newMapper() @@ -115,10 +117,64 @@ func init() { requestContextMapper = api.NewRequestContextMapper() } +// defaultAPIServer exposes nested objects for testability. +type defaultAPIServer struct { + http.Handler + group *APIGroupVersion +} + +// uses the default settings +func handle(storage map[string]RESTStorage) http.Handler { + return handleInternal(storage, admissionControl, mapper, selfLinker) +} + +// tests with a deny admission controller +func handleDeny(storage map[string]RESTStorage) http.Handler { + return handleInternal(storage, deny.NewAlwaysDeny(), mapper, selfLinker) +} + +// tests using the new namespace scope mechanism +func handleNamespaced(storage map[string]RESTStorage) http.Handler { + return handleInternal(storage, admissionControl, namespaceMapper, selfLinker) +} + +// tests using a custom self linker +func handleLinker(storage map[string]RESTStorage, selfLinker runtime.SelfLinker) http.Handler { + return handleInternal(storage, admissionControl, mapper, selfLinker) +} + +func handleInternal(storage map[string]RESTStorage, admissionControl admission.Interface, mapper meta.RESTMapper, selfLinker runtime.SelfLinker) http.Handler { + group := &APIGroupVersion{ + Storage: storage, + + Mapper: mapper, + + Root: "/api", + Version: testVersion, + + Creater: api.Scheme, + Typer: api.Scheme, + Codec: codec, + Linker: selfLinker, + + Admit: admissionControl, + Context: requestContextMapper, + } + container := restful.NewContainer() + container.Router(restful.CurlyRouter{}) + mux := container.ServeMux + group.InstallREST(container) + ws := new(restful.WebService) + InstallSupport(mux, ws) + container.Add(ws) + return &defaultAPIServer{mux, group} +} + type Simple struct { api.TypeMeta `json:",inline"` api.ObjectMeta `json:"metadata"` - Other string `json:"other,omitempty"` + Other string `json:"other,omitempty"` + Labels map[string]string `json:"labels,omitempty"` } func (*Simple) IsAnAPIObject() {} @@ -147,20 +203,23 @@ func TestSimpleSetupRight(t *testing.T) { } type SimpleRESTStorage struct { - errors map[string]error - list []Simple - item Simple - deleted string + errors map[string]error + list []Simple + item Simple + updated *Simple created *Simple + deleted string + deleteOptions *api.DeleteOptions + actualNamespace string namespacePresent bool // These are set when Watch is called fakeWatch *watch.FakeWatcher requestedLabelSelector labels.Selector - requestedFieldSelector labels.Selector + requestedFieldSelector fields.Selector requestedResourceVersion string requestedResourceNamespace string @@ -174,7 +233,7 @@ type SimpleRESTStorage struct { injectedFunction func(obj runtime.Object) (returnObj runtime.Object, err error) } -func (storage *SimpleRESTStorage) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) { +func (storage *SimpleRESTStorage) List(ctx api.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { storage.checkContext(ctx) result := &SimpleList{ Items: storage.list, @@ -191,9 +250,10 @@ func (storage *SimpleRESTStorage) checkContext(ctx api.Context) { storage.actualNamespace, storage.namespacePresent = api.NamespaceFrom(ctx) } -func (storage *SimpleRESTStorage) Delete(ctx api.Context, id string) (runtime.Object, error) { +func (storage *SimpleRESTStorage) Delete(ctx api.Context, id string, options *api.DeleteOptions) (runtime.Object, error) { storage.checkContext(ctx) storage.deleted = id + storage.deleteOptions = options if err := storage.errors["delete"]; err != nil { return nil, err } @@ -240,7 +300,7 @@ func (storage *SimpleRESTStorage) Update(ctx api.Context, obj runtime.Object) (r } // Implement ResourceWatcher. -func (storage *SimpleRESTStorage) Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (storage *SimpleRESTStorage) Watch(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { storage.checkContext(ctx) storage.requestedLabelSelector = label storage.requestedFieldSelector = field @@ -268,6 +328,14 @@ func (storage *SimpleRESTStorage) ResourceLocation(ctx api.Context, id string) ( return storage.resourceLocation, nil } +type LegacyRESTStorage struct { + *SimpleRESTStorage +} + +func (storage LegacyRESTStorage) Delete(ctx api.Context, id string) (runtime.Object, error) { + return storage.SimpleRESTStorage.Delete(ctx, id, nil) +} + func extractBody(response *http.Response, object runtime.Object) (string, error) { defer response.Body.Close() body, err := ioutil.ReadAll(response.Body) @@ -285,21 +353,21 @@ func TestNotFound(t *testing.T) { Status int } cases := map[string]T{ - "PATCH method": {"PATCH", "/prefix/version/foo", http.StatusMethodNotAllowed}, - "GET long prefix": {"GET", "/prefix/", http.StatusNotFound}, - "GET missing storage": {"GET", "/prefix/version/blah", http.StatusNotFound}, - "GET with extra segment": {"GET", "/prefix/version/foo/bar/baz", http.StatusNotFound}, - "POST with extra segment": {"POST", "/prefix/version/foo/bar", http.StatusMethodNotAllowed}, - "DELETE without extra segment": {"DELETE", "/prefix/version/foo", http.StatusMethodNotAllowed}, - "DELETE with extra segment": {"DELETE", "/prefix/version/foo/bar/baz", http.StatusNotFound}, - "PUT without extra segment": {"PUT", "/prefix/version/foo", http.StatusMethodNotAllowed}, - "PUT with extra segment": {"PUT", "/prefix/version/foo/bar/baz", http.StatusNotFound}, - "watch missing storage": {"GET", "/prefix/version/watch/", http.StatusNotFound}, - "watch with bad method": {"POST", "/prefix/version/watch/foo/bar", http.StatusMethodNotAllowed}, - } - handler := Handle(map[string]RESTStorage{ + "PATCH method": {"PATCH", "/api/version/foo", http.StatusMethodNotAllowed}, + "GET long prefix": {"GET", "/api/", http.StatusNotFound}, + "GET missing storage": {"GET", "/api/version/blah", http.StatusNotFound}, + "GET with extra segment": {"GET", "/api/version/foo/bar/baz", http.StatusNotFound}, + "POST with extra segment": {"POST", "/api/version/foo/bar", http.StatusMethodNotAllowed}, + "DELETE without extra segment": {"DELETE", "/api/version/foo", http.StatusMethodNotAllowed}, + "DELETE with extra segment": {"DELETE", "/api/version/foo/bar/baz", http.StatusNotFound}, + "PUT without extra segment": {"PUT", "/api/version/foo", http.StatusMethodNotAllowed}, + "PUT with extra segment": {"PUT", "/api/version/foo/bar/baz", http.StatusNotFound}, + "watch missing storage": {"GET", "/api/version/watch/", http.StatusNotFound}, + "watch with bad method": {"POST", "/api/version/watch/foo/bar", http.StatusMethodNotAllowed}, + } + handler := handle(map[string]RESTStorage{ "foo": &SimpleRESTStorage{}, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + }) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -339,19 +407,19 @@ func TestUnimplementedRESTStorage(t *testing.T) { ErrCode int } cases := map[string]T{ - "GET object": {"GET", "/prefix/version/foo/bar", http.StatusNotFound}, - "GET list": {"GET", "/prefix/version/foo", http.StatusNotFound}, - "POST list": {"POST", "/prefix/version/foo", http.StatusNotFound}, - "PUT object": {"PUT", "/prefix/version/foo/bar", http.StatusNotFound}, - "DELETE object": {"DELETE", "/prefix/version/foo/bar", http.StatusNotFound}, - "watch list": {"GET", "/prefix/version/watch/foo", http.StatusNotFound}, - "watch object": {"GET", "/prefix/version/watch/foo/bar", http.StatusNotFound}, - "proxy object": {"GET", "/prefix/version/proxy/foo/bar", http.StatusNotFound}, - "redirect object": {"GET", "/prefix/version/redirect/foo/bar", http.StatusNotFound}, - } - handler := Handle(map[string]RESTStorage{ + "GET object": {"GET", "/api/version/foo/bar", http.StatusNotFound}, + "GET list": {"GET", "/api/version/foo", http.StatusNotFound}, + "POST list": {"POST", "/api/version/foo", http.StatusNotFound}, + "PUT object": {"PUT", "/api/version/foo/bar", http.StatusNotFound}, + "DELETE object": {"DELETE", "/api/version/foo/bar", http.StatusNotFound}, + "watch list": {"GET", "/api/version/watch/foo", http.StatusNotFound}, + "watch object": {"GET", "/api/version/watch/foo/bar", http.StatusNotFound}, + "proxy object": {"GET", "/api/version/proxy/foo/bar", http.StatusNotFound}, + "redirect object": {"GET", "/api/version/redirect/foo/bar", http.StatusNotFound}, + } + handler := handle(map[string]RESTStorage{ "foo": UnimplementedRESTStorage{}, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + }) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -376,7 +444,7 @@ func TestUnimplementedRESTStorage(t *testing.T) { } func TestVersion(t *testing.T) { - handler := Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{}) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -409,14 +477,14 @@ func TestList(t *testing.T) { selfLink string legacy bool }{ - {"/prefix/version/simple", "", "/prefix/version/simple?namespace=", true}, - {"/prefix/version/simple?namespace=other", "other", "/prefix/version/simple?namespace=other", true}, + {"/api/version/simple", "", "/api/version/simple?namespace=", true}, + {"/api/version/simple?namespace=other", "other", "/api/version/simple?namespace=other", true}, // list items across all namespaces - {"/prefix/version/simple?namespace=", "", "/prefix/version/simple?namespace=", true}, - {"/prefix/version/namespaces/default/simple", "default", "/prefix/version/namespaces/default/simple", false}, - {"/prefix/version/namespaces/other/simple", "other", "/prefix/version/namespaces/other/simple", false}, + {"/api/version/simple?namespace=", "", "/api/version/simple?namespace=", true}, + {"/api/version/namespaces/default/simple", "default", "/api/version/namespaces/default/simple", false}, + {"/api/version/namespaces/other/simple", "other", "/api/version/namespaces/other/simple", false}, // list items across all namespaces - {"/prefix/version/simple", "", "/prefix/version/simple", false}, + {"/api/version/simple", "", "/api/version/simple", false}, } for i, testCase := range testCases { storage := map[string]RESTStorage{} @@ -429,9 +497,9 @@ func TestList(t *testing.T) { } var handler http.Handler if testCase.legacy { - handler = Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler = handleLinker(storage, selfLinker) } else { - handler = Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, namespaceMapper) + handler = handleInternal(storage, admissionControl, namespaceMapper, selfLinker) } server := httptest.NewServer(handler) defer server.Close() @@ -462,11 +530,11 @@ func TestErrorList(t *testing.T) { errors: map[string]error{"list": fmt.Errorf("test Error")}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple") + resp, err := http.Get(server.URL + "/api/version/simple") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -487,11 +555,11 @@ func TestNonEmptyList(t *testing.T) { }, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple") + resp, err := http.Get(server.URL + "/api/version/simple") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -515,10 +583,10 @@ func TestNonEmptyList(t *testing.T) { if listOut.Items[0].Other != simpleStorage.list[0].Other { t.Errorf("Unexpected data: %#v, %s", listOut.Items[0], string(body)) } - if listOut.SelfLink != "/prefix/version/simple?namespace=" { + if listOut.SelfLink != "/api/version/simple?namespace=" { t.Errorf("unexpected list self link: %#v", listOut) } - expectedSelfLink := "/prefix/version/simple/something?namespace=other" + expectedSelfLink := "/api/version/simple/something?namespace=other" if listOut.Items[0].ObjectMeta.SelfLink != expectedSelfLink { t.Errorf("Unexpected data: %#v, %s", listOut.Items[0].ObjectMeta.SelfLink, expectedSelfLink) } @@ -535,11 +603,11 @@ func TestSelfLinkSkipsEmptyName(t *testing.T) { }, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple") + resp, err := http.Get(server.URL + "/api/version/simple") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -562,7 +630,7 @@ func TestSelfLinkSkipsEmptyName(t *testing.T) { if listOut.Items[0].Other != simpleStorage.list[0].Other { t.Errorf("Unexpected data: %#v, %s", listOut.Items[0], string(body)) } - if listOut.SelfLink != "/prefix/version/simple?namespace=" { + if listOut.SelfLink != "/api/version/simple?namespace=" { t.Errorf("unexpected list self link: %#v", listOut) } expectedSelfLink := "" @@ -580,16 +648,16 @@ func TestGet(t *testing.T) { } selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/prefix/version/simple/id?namespace=default", + expectedSet: "/api/version/simple/id?namespace=default", name: "id", namespace: "default", } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(storage, selfLinker) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple/id") + resp, err := http.Get(server.URL + "/api/version/simple/id") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -619,16 +687,16 @@ func TestGetAlternateSelfLink(t *testing.T) { } selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/prefix/version/simple/id?namespace=test", + expectedSet: "/api/version/simple/id?namespace=test", name: "id", namespace: "test", } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, legacyNamespaceMapper) + handler := handleLinker(storage, selfLinker) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple/id?namespace=test") + resp, err := http.Get(server.URL + "/api/version/simple/id?namespace=test") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -657,16 +725,16 @@ func TestGetNamespaceSelfLink(t *testing.T) { } selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/prefix/version/namespaces/foo/simple/id", + expectedSet: "/api/version/namespaces/foo/simple/id", name: "id", namespace: "foo", } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, namespaceMapper) + handler := handleInternal(storage, admissionControl, namespaceMapper, selfLinker) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/namespaces/foo/simple/id") + resp, err := http.Get(server.URL + "/api/version/namespaces/foo/simple/id") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -691,11 +759,11 @@ func TestGetMissing(t *testing.T) { errors: map[string]error{"get": apierrs.NewNotFound("simple", "id")}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() - resp, err := http.Get(server.URL + "/prefix/version/simple/id") + resp, err := http.Get(server.URL + "/api/version/simple/id") if err != nil { t.Errorf("unexpected error: %v", err) } @@ -710,12 +778,12 @@ func TestDelete(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} - request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil) + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, nil) res, err := client.Do(request) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -728,17 +796,113 @@ func TestDelete(t *testing.T) { } } +func TestDeleteWithOptions(t *testing.T) { + storage := map[string]RESTStorage{} + simpleStorage := SimpleRESTStorage{} + ID := "id" + storage["simple"] = &simpleStorage + handler := handle(storage) + server := httptest.NewServer(handler) + defer server.Close() + + grace := int64(300) + item := &api.DeleteOptions{ + GracePeriodSeconds: &grace, + } + body, err := codec.Encode(item) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + client := http.Client{} + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) + res, err := client.Do(request) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("unexpected response: %s %#v", request.URL, res) + s, _ := ioutil.ReadAll(res.Body) + t.Logf(string(s)) + } + if simpleStorage.deleted != ID { + t.Errorf("Unexpected delete: %s, expected %s", simpleStorage.deleted, ID) + } + if !api.Semantic.DeepEqual(simpleStorage.deleteOptions, item) { + t.Errorf("unexpected delete options: %s", util.ObjectDiff(simpleStorage.deleteOptions, item)) + } +} + +func TestLegacyDelete(t *testing.T) { + storage := map[string]RESTStorage{} + simpleStorage := SimpleRESTStorage{} + ID := "id" + storage["simple"] = LegacyRESTStorage{&simpleStorage} + var _ RESTDeleter = storage["simple"].(LegacyRESTStorage) + handler := handle(storage) + server := httptest.NewServer(handler) + defer server.Close() + + client := http.Client{} + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, nil) + res, err := client.Do(request) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("unexpected response: %#v", res) + } + if simpleStorage.deleted != ID { + t.Errorf("Unexpected delete: %s, expected %s", simpleStorage.deleted, ID) + } + if simpleStorage.deleteOptions != nil { + t.Errorf("unexpected delete options: %#v", simpleStorage.deleteOptions) + } +} + +func TestLegacyDeleteIgnoresOptions(t *testing.T) { + storage := map[string]RESTStorage{} + simpleStorage := SimpleRESTStorage{} + ID := "id" + storage["simple"] = LegacyRESTStorage{&simpleStorage} + handler := handle(storage) + server := httptest.NewServer(handler) + defer server.Close() + + item := api.NewDeleteOptions(300) + body, err := codec.Encode(item) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + client := http.Client{} + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) + res, err := client.Do(request) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("unexpected response: %#v", res) + } + if simpleStorage.deleted != ID { + t.Errorf("Unexpected delete: %s, expected %s", simpleStorage.deleted, ID) + } + if simpleStorage.deleteOptions != nil { + t.Errorf("unexpected delete options: %#v", simpleStorage.deleteOptions) + } +} + func TestDeleteInvokesAdmissionControl(t *testing.T) { storage := map[string]RESTStorage{} simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny(), requestContextMapper, mapper) + handler := handleDeny(storage) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} - request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil) + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, nil) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -755,12 +919,12 @@ func TestDeleteMissing(t *testing.T) { errors: map[string]error{"delete": apierrs.NewNotFound("simple", ID)}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} - request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil) + request, err := http.NewRequest("DELETE", server.URL+"/api/version/simple/"+ID, nil) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -771,6 +935,70 @@ func TestDeleteMissing(t *testing.T) { } } +func TestPatch(t *testing.T) { + storage := map[string]RESTStorage{} + ID := "id" + item := &Simple{ + ObjectMeta: api.ObjectMeta{ + Name: ID, + Namespace: "", // update should allow the client to send an empty namespace + }, + Other: "bar", + } + simpleStorage := SimpleRESTStorage{item: *item} + storage["simple"] = &simpleStorage + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/api/version/simple/" + ID + "?namespace=default", + name: ID, + namespace: api.NamespaceDefault, + } + handler := handleLinker(storage, selfLinker) + server := httptest.NewServer(handler) + defer server.Close() + + client := http.Client{} + request, err := http.NewRequest("PATCH", server.URL+"/api/version/simple/"+ID, bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`))) + _, err = client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if simpleStorage.updated == nil || simpleStorage.updated.Labels["foo"] != "bar" { + t.Errorf("Unexpected update value %#v, expected %#v.", simpleStorage.updated, item) + } + if !selfLinker.called { + t.Errorf("Never set self link") + } +} + +func TestPatchRequiresMatchingName(t *testing.T) { + storage := map[string]RESTStorage{} + ID := "id" + item := &Simple{ + ObjectMeta: api.ObjectMeta{ + Name: ID, + Namespace: "", // update should allow the client to send an empty namespace + }, + Other: "bar", + } + simpleStorage := SimpleRESTStorage{item: *item} + storage["simple"] = &simpleStorage + handler := handle(storage) + server := httptest.NewServer(handler) + defer server.Close() + + client := http.Client{} + request, err := http.NewRequest("PATCH", server.URL+"/api/version/simple/"+ID, bytes.NewReader([]byte(`{"metadata":{"name":"idbar"}}`))) + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusBadRequest { + t.Errorf("Unexpected response %#v", response) + } +} + func TestUpdate(t *testing.T) { storage := map[string]RESTStorage{} simpleStorage := SimpleRESTStorage{} @@ -778,11 +1006,11 @@ func TestUpdate(t *testing.T) { storage["simple"] = &simpleStorage selfLinker := &setTestSelfLinker{ t: t, - expectedSet: "/prefix/version/simple/" + ID + "?namespace=default", + expectedSet: "/api/version/simple/" + ID + "?namespace=default", name: ID, namespace: api.NamespaceDefault, } - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(storage, selfLinker) server := httptest.NewServer(handler) defer server.Close() @@ -800,7 +1028,7 @@ func TestUpdate(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) _, err = client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -819,7 +1047,7 @@ func TestUpdateInvokesAdmissionControl(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny(), requestContextMapper, mapper) + handler := handleDeny(storage) server := httptest.NewServer(handler) defer server.Close() @@ -837,7 +1065,7 @@ func TestUpdateInvokesAdmissionControl(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -852,7 +1080,7 @@ func TestUpdateRequiresMatchingName(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny(), requestContextMapper, mapper) + handler := handleDeny(storage) server := httptest.NewServer(handler) defer server.Close() @@ -866,7 +1094,7 @@ func TestUpdateRequiresMatchingName(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -881,7 +1109,7 @@ func TestUpdateAllowsMissingNamespace(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() @@ -898,7 +1126,7 @@ func TestUpdateAllowsMissingNamespace(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -918,7 +1146,7 @@ func TestUpdateAllowsMismatchedNamespaceOnError(t *testing.T) { t: t, err: fmt.Errorf("test error"), } - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(storage, selfLinker) server := httptest.NewServer(handler) defer server.Close() @@ -936,7 +1164,7 @@ func TestUpdateAllowsMismatchedNamespaceOnError(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) _, err = client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -955,7 +1183,7 @@ func TestUpdatePreventsMismatchedNamespace(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() @@ -973,7 +1201,7 @@ func TestUpdatePreventsMismatchedNamespace(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -990,7 +1218,7 @@ func TestUpdateMissing(t *testing.T) { errors: map[string]error{"update": apierrs.NewNotFound("simple", ID)}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(storage) server := httptest.NewServer(handler) defer server.Close() @@ -1007,7 +1235,7 @@ func TestUpdateMissing(t *testing.T) { } client := http.Client{} - request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/"+ID, bytes.NewReader(body)) response, err := client.Do(request) if err != nil { t.Errorf("unexpected error: %v", err) @@ -1018,20 +1246,20 @@ func TestUpdateMissing(t *testing.T) { } func TestCreateNotFound(t *testing.T) { - handler := Handle(map[string]RESTStorage{ + handler := handle(map[string]RESTStorage{ "simple": &SimpleRESTStorage{ // storage.Create can fail with not found error in theory. // See https://github.com/GoogleCloudPlatform/kubernetes/pull/486#discussion_r15037092. errors: map[string]error{"create": apierrs.NewNotFound("simple", "id")}, }, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + }) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} simple := &Simple{Other: "foo"} data, _ := codec.Encode(simple) - request, err := http.NewRequest("POST", server.URL+"/prefix/version/simple", bytes.NewBuffer(data)) + request, err := http.NewRequest("POST", server.URL+"/api/version/simple", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1046,11 +1274,59 @@ func TestCreateNotFound(t *testing.T) { } } +func TestCreateChecksDecode(t *testing.T) { + handler := handle(map[string]RESTStorage{"simple": &SimpleRESTStorage{}}) + server := httptest.NewServer(handler) + defer server.Close() + client := http.Client{} + + simple := &api.Pod{} + data, _ := codec.Encode(simple) + request, err := http.NewRequest("POST", server.URL+"/api/version/simple", bytes.NewBuffer(data)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusBadRequest { + t.Errorf("Unexpected response %#v", response) + } + if b, _ := ioutil.ReadAll(response.Body); !strings.Contains(string(b), "must be of type Simple") { + t.Errorf("unexpected response: %s", string(b)) + } +} + +func TestUpdateChecksDecode(t *testing.T) { + handler := handle(map[string]RESTStorage{"simple": &SimpleRESTStorage{}}) + server := httptest.NewServer(handler) + defer server.Close() + client := http.Client{} + + simple := &api.Pod{} + data, _ := codec.Encode(simple) + request, err := http.NewRequest("PUT", server.URL+"/api/version/simple/bar", bytes.NewBuffer(data)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusBadRequest { + t.Errorf("Unexpected response %#v", response) + } + if b, _ := ioutil.ReadAll(response.Body); !strings.Contains(string(b), "must be of type Simple") { + t.Errorf("unexpected response: %s", string(b)) + } +} + func TestParseTimeout(t *testing.T) { - if d := parseTimeout(""); d != 30*time.Second { + if d := parseTimeout(""); d != 2*time.Minute { t.Errorf("blank timeout produces %v", d) } - if d := parseTimeout("not a timeout"); d != 30*time.Second { + if d := parseTimeout("not a timeout"); d != 2*time.Minute { t.Errorf("bad timeout produces %v", d) } if d := parseTimeout("10s"); d != 10*time.Second { @@ -1089,11 +1365,9 @@ func TestCreate(t *testing.T) { t: t, name: "bar", namespace: "default", - expectedSet: "/prefix/version/foo/bar?namespace=default", + expectedSet: "/api/version/foo/bar?namespace=default", } - handler := Handle(map[string]RESTStorage{ - "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(map[string]RESTStorage{"foo": &storage}, selfLinker) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -1102,7 +1376,7 @@ func TestCreate(t *testing.T) { Other: "bar", } data, _ := codec.Encode(simple) - request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data)) + request, err := http.NewRequest("POST", server.URL+"/api/version/foo", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1147,11 +1421,9 @@ func TestCreateInNamespace(t *testing.T) { t: t, name: "bar", namespace: "other", - expectedSet: "/prefix/version/foo/bar?namespace=other", + expectedSet: "/api/version/foo/bar?namespace=other", } - handler := Handle(map[string]RESTStorage{ - "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handleLinker(map[string]RESTStorage{"foo": &storage}, selfLinker) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -1160,7 +1432,7 @@ func TestCreateInNamespace(t *testing.T) { Other: "bar", } data, _ := codec.Encode(simple) - request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo?namespace=other", bytes.NewBuffer(data)) + request, err := http.NewRequest("POST", server.URL+"/api/version/foo?namespace=other", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1205,11 +1477,9 @@ func TestCreateInvokesAdmissionControl(t *testing.T) { t: t, name: "bar", namespace: "other", - expectedSet: "/prefix/version/foo/bar?namespace=other", + expectedSet: "/api/version/foo/bar?namespace=other", } - handler := Handle(map[string]RESTStorage{ - "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny(), requestContextMapper, mapper) + handler := handleInternal(map[string]RESTStorage{"foo": &storage}, deny.NewAlwaysDeny(), mapper, selfLinker) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -1218,7 +1488,7 @@ func TestCreateInvokesAdmissionControl(t *testing.T) { Other: "bar", } data, _ := codec.Encode(simple) - request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo?namespace=other", bytes.NewBuffer(data)) + request, err := http.NewRequest("POST", server.URL+"/api/version/foo?namespace=other", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -1269,11 +1539,11 @@ func TestDelayReturnsError(t *testing.T) { return nil, apierrs.NewAlreadyExists("foo", "bar") }, } - handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": &storage}) server := httptest.NewServer(handler) defer server.Close() - status := expectApiStatus(t, "DELETE", fmt.Sprintf("%s/prefix/version/foo/bar", server.URL), nil, http.StatusConflict) + status := expectApiStatus(t, "DELETE", fmt.Sprintf("%s/api/version/foo/bar", server.URL), nil, http.StatusConflict) if status.Status != api.StatusFailure || status.Message == "" || status.Details == nil || status.Reason != api.StatusReasonAlreadyExists { t.Errorf("Unexpected status %#v", status) } @@ -1333,15 +1603,15 @@ func TestCreateTimeout(t *testing.T) { return obj, nil }, } - handler := Handle(map[string]RESTStorage{ + handler := handle(map[string]RESTStorage{ "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper) + }) server := httptest.NewServer(handler) defer server.Close() simple := &Simple{Other: "foo"} data, _ := codec.Encode(simple) - itemOut := expectApiStatus(t, "POST", server.URL+"/prefix/version/foo?timeout=4ms", data, apierrs.StatusServerTimeout) + itemOut := expectApiStatus(t, "POST", server.URL+"/api/version/foo?timeout=4ms", data, apierrs.StatusServerTimeout) if itemOut.Status != api.StatusFailure || itemOut.Reason != api.StatusReasonTimeout { t.Errorf("Unexpected status %#v", itemOut) } @@ -1367,7 +1637,7 @@ func TestCORSAllowedOrigins(t *testing.T) { } handler := CORS( - Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker, admissionControl, requestContextMapper, mapper), + handle(map[string]RESTStorage{}), allowedOriginRegexps, nil, nil, "true", ) server := httptest.NewServer(handler) diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/async.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/async.go deleted file mode 100644 index ca1d0aa32cf1..000000000000 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/async.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -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 apiserver - -import ( - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" -) - -// WorkFunc is used to perform any time consuming work for an api call, after -// the input has been validated. Pass one of these to MakeAsync to create an -// appropriate return value for the Update, Delete, and Create methods. -type WorkFunc func() (result runtime.Object, err error) - -// MakeAsync takes a function and executes it, delivering the result in the way required -// by RESTStorage's Update, Delete, and Create methods. -func MakeAsync(fn WorkFunc) <-chan RESTResult { - channel := make(chan RESTResult) - go func() { - defer util.HandleCrash() - obj, err := fn() - if err != nil { - channel <- RESTResult{Object: errToAPIStatus(err)} - } else { - channel <- RESTResult{Object: obj} - } - // 'close' is used to signal that no further values will - // be written to the channel. Not strictly necessary, but - // also won't hurt. - close(channel) - }() - return channel -} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/handlers.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/handlers.go index 04d05b2d7141..a85e2f0ecbf6 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/handlers.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/handlers.go @@ -19,6 +19,7 @@ package apiserver import ( "fmt" "net/http" + "path" "regexp" "runtime/debug" "strings" @@ -214,8 +215,22 @@ type APIRequestInfo struct { Kind string // Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in. Name string - // Parts are the path parts for the request relative to /{resource}/{name} + // Parts are the path parts for the request, always starting with /{resource}/{name} Parts []string + // Raw is the unparsed form of everything other than parts. + // Raw + Parts = complete URL path + Raw []string +} + +// URLPath returns the URL path for this request, including /{resource}/{name} if present but nothing +// following that. +func (info APIRequestInfo) URLPath() string { + p := info.Parts + if n := len(p); n > 2 { + // Only take resource and name + p = p[:2] + } + return path.Join("/", path.Join(info.Raw...), path.Join(p...)) } type APIRequestInfoResolver struct { @@ -247,9 +262,11 @@ type APIRequestInfoResolver struct { // /api/{version}/* // /api/{version}/* func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIRequestInfo, error) { - requestInfo := APIRequestInfo{} + requestInfo := APIRequestInfo{ + Raw: splitPath(req.URL.Path), + } - currentParts := splitPath(req.URL.Path) + currentParts := requestInfo.Raw if len(currentParts) < 1 { return requestInfo, fmt.Errorf("Unable to determine kind and namespace from an empty URL path") } @@ -322,6 +339,8 @@ func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIReques // parsing successful, so we now know the proper value for .Parts requestInfo.Parts = currentParts + // Raw should have everything not in Parts + requestInfo.Raw = requestInfo.Raw[:len(requestInfo.Raw)-len(currentParts)] // if there's another part remaining after the kind, then that's the resource name if len(requestInfo.Parts) >= 2 { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/handlers_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/handlers_test.go index 59509c9699f1..8ff475b24c66 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/handlers_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/handlers_test.go @@ -20,6 +20,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strings" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -142,6 +143,9 @@ func TestGetAPIRequestInfo(t *testing.T) { if !reflect.DeepEqual(successCase.expectedParts, apiRequestInfo.Parts) { t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, apiRequestInfo.Parts) } + if e, a := strings.Split(successCase.url, "?")[0], apiRequestInfo.URLPath(); e != a { + t.Errorf("Expected %v, got %v", e, a) + } } errorCases := map[string]string{ diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/interfaces.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/interfaces.go index 233bd12236ed..450481bdb9e7 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/interfaces.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/interfaces.go @@ -18,6 +18,7 @@ package apiserver import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" @@ -38,7 +39,7 @@ type RESTLister interface { // This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object) NewList() runtime.Object // List selects resources in the storage which match to the selector. - List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) + List(ctx api.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) } type RESTGetter interface { @@ -57,6 +58,27 @@ type RESTDeleter interface { Delete(ctx api.Context, id string) (runtime.Object, error) } +type RESTGracefulDeleter interface { + // Delete finds a resource in the storage and deletes it. + // If options are provided, the resource will attempt to honor them or return an invalid + // request error. + // Although it can return an arbitrary error value, IsNotFound(err) is true for the + // returned error value err when the specified resource is not found. + // Delete *may* return the object that was deleted, or a status object indicating additional + // information about deletion. + Delete(ctx api.Context, id string, options *api.DeleteOptions) (runtime.Object, error) +} + +// GracefulDeleteAdapter adapts the RESTDeleter interface to RESTGracefulDeleter +type GracefulDeleteAdapter struct { + RESTDeleter +} + +// Delete implements RESTGracefulDeleter in terms of RESTDeleter +func (w GracefulDeleteAdapter) Delete(ctx api.Context, id string, options *api.DeleteOptions) (runtime.Object, error) { + return w.RESTDeleter.Delete(ctx, id) +} + type RESTCreater interface { // New returns an empty object that can be used with Create after request data has been put into it. // This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object) @@ -77,6 +99,11 @@ type RESTUpdater interface { Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) } +type RESTPatcher interface { + RESTGetter + RESTUpdater +} + // RESTResult indicates the result of a REST transformation. type RESTResult struct { // The result of this operation. May be nil if the operation has no meaningful @@ -95,7 +122,7 @@ type ResourceWatcher interface { // are supported; an error should be returned if 'field' tries to select on a field that // isn't supported. 'resourceVersion' allows for continuing/starting a watch at a // particular version. - Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) + Watch(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } // Redirector know how to return a remote resource's location. diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/operation.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/operation.go deleted file mode 100644 index f0c88de6cfb4..000000000000 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/operation.go +++ /dev/null @@ -1,154 +0,0 @@ -/* -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 apiserver - -import ( - "strconv" - "sync" - "sync/atomic" - "time" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" -) - -// Operation represents an ongoing action which the server is performing. -type Operation struct { - ID string - result RESTResult - onReceive func(RESTResult) - awaiting <-chan RESTResult - finished *time.Time - lock sync.Mutex - notify chan struct{} -} - -// Operations tracks all the ongoing operations. -type Operations struct { - // Access only using functions from atomic. - lastID int64 - - // 'lock' guards the ops map. - lock sync.Mutex - ops map[string]*Operation -} - -// NewOperations returns a new Operations repository. -func NewOperations() *Operations { - ops := &Operations{ - ops: map[string]*Operation{}, - } - go util.Forever(func() { ops.expire(10 * time.Minute) }, 5*time.Minute) - return ops -} - -// NewOperation adds a new operation. It is lock-free. 'onReceive' will be called -// with the value read from 'from', when it is read. -func (ops *Operations) NewOperation(from <-chan RESTResult, onReceive func(RESTResult)) *Operation { - id := atomic.AddInt64(&ops.lastID, 1) - op := &Operation{ - ID: strconv.FormatInt(id, 10), - awaiting: from, - onReceive: onReceive, - notify: make(chan struct{}), - } - go op.wait() - go ops.insert(op) - return op -} - -// insert inserts op into the ops map. -func (ops *Operations) insert(op *Operation) { - ops.lock.Lock() - defer ops.lock.Unlock() - ops.ops[op.ID] = op -} - -// Get returns the operation with the given ID, or nil. -func (ops *Operations) Get(id string) *Operation { - ops.lock.Lock() - defer ops.lock.Unlock() - return ops.ops[id] -} - -// expire garbage collect operations that have finished longer than maxAge ago. -func (ops *Operations) expire(maxAge time.Duration) { - ops.lock.Lock() - defer ops.lock.Unlock() - keep := map[string]*Operation{} - limitTime := time.Now().Add(-maxAge) - for id, op := range ops.ops { - if !op.expired(limitTime) { - keep[id] = op - } - } - ops.ops = keep -} - -// wait waits forever for the operation to complete; call via go when -// the operation is created. Sets op.finished when the operation -// does complete, and closes the notify channel, in case there -// are any WaitFor() calls in progress. -// Does not keep op locked while waiting. -func (op *Operation) wait() { - defer util.HandleCrash() - result := <-op.awaiting - - op.lock.Lock() - defer op.lock.Unlock() - if op.onReceive != nil { - op.onReceive(result) - } - op.result = result - finished := time.Now() - op.finished = &finished - close(op.notify) -} - -// WaitFor waits for the specified duration, or until the operation finishes, -// whichever happens first. -func (op *Operation) WaitFor(timeout time.Duration) { - select { - case <-time.After(timeout): - case <-op.notify: - } -} - -// expired returns true if this operation finished before limitTime. -func (op *Operation) expired(limitTime time.Time) bool { - op.lock.Lock() - defer op.lock.Unlock() - if op.finished == nil { - return false - } - return op.finished.Before(limitTime) -} - -// StatusOrResult returns status information or the result of the operation if it is complete, -// with a bool indicating true in the latter case. -func (op *Operation) StatusOrResult() (description RESTResult, finished bool) { - op.lock.Lock() - defer op.lock.Unlock() - - if op.finished == nil { - return RESTResult{Object: &api.Status{ - Status: api.StatusFailure, - Reason: api.StatusReasonTimeout, - }}, false - } - return op.result, true -} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/proxy.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/proxy.go index bfea64eea932..88e052cdb7d7 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/proxy.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/proxy.go @@ -90,7 +90,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { var apiResource string var httpCode int reqStart := time.Now() - defer monitor("proxy", verb, apiResource, httpCode, reqStart) + defer monitor("proxy", &verb, &apiResource, &httpCode, reqStart) requestInfo, err := r.apiRequestInfoResolver.GetAPIRequestInfo(req) if err != nil { @@ -147,12 +147,6 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { httpCode = status.Code return } - if location == "" { - httplog.LogOf(req, w).Addf("ResourceLocation for %v returned ''", id) - notFound(w, req) - httpCode = http.StatusNotFound - return - } destURL, err := url.Parse(location) if err != nil { @@ -182,64 +176,73 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // TODO convert this entire proxy to an UpgradeAwareProxy similar to // https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go. // That proxy needs to be modified to support multiple backends, not just 1. + if r.tryUpgrade(w, req, newReq, destURL) { + return + } + + proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: destURL.Host}) + proxy.Transport = &proxyTransport{ + proxyScheme: req.URL.Scheme, + proxyHost: req.URL.Host, + proxyPathPrepend: requestInfo.URLPath(), + } + proxy.FlushInterval = 200 * time.Millisecond + proxy.ServeHTTP(w, newReq) +} + +// tryUpgrade returns true if the request was handled. +func (r *ProxyHandler) tryUpgrade(w http.ResponseWriter, req, newReq *http.Request, destURL *url.URL) bool { connectionHeader := strings.ToLower(req.Header.Get(httpstream.HeaderConnection)) - if strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) && len(req.Header.Get(httpstream.HeaderUpgrade)) > 0 { - //TODO support TLS? Doesn't look like proxyTransport does anything special ... - dialAddr := netutil.CanonicalAddr(destURL) - backendConn, err := net.Dial("tcp", dialAddr) - if err != nil { - status := errToAPIStatus(err) - writeJSON(status.Code, r.codec, status, w) - return - } - defer backendConn.Close() + if !strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) || len(req.Header.Get(httpstream.HeaderUpgrade)) == 0 { + return false + } + //TODO support TLS? Doesn't look like proxyTransport does anything special ... + dialAddr := netutil.CanonicalAddr(destURL) + backendConn, err := net.Dial("tcp", dialAddr) + if err != nil { + status := errToAPIStatus(err) + writeJSON(status.Code, r.codec, status, w) + return true + } + defer backendConn.Close() - // TODO should we use _ (a bufio.ReadWriter) instead of requestHijackedConn - // when copying between the client and the backend? Docker doesn't when they - // hijack, just for reference... - requestHijackedConn, _, err := w.(http.Hijacker).Hijack() - if err != nil { - status := errToAPIStatus(err) - writeJSON(status.Code, r.codec, status, w) - return - } - defer requestHijackedConn.Close() + // TODO should we use _ (a bufio.ReadWriter) instead of requestHijackedConn + // when copying between the client and the backend? Docker doesn't when they + // hijack, just for reference... + requestHijackedConn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + status := errToAPIStatus(err) + writeJSON(status.Code, r.codec, status, w) + return true + } + defer requestHijackedConn.Close() - if err = newReq.Write(backendConn); err != nil { - status := errToAPIStatus(err) - writeJSON(status.Code, r.codec, status, w) - return + if err = newReq.Write(backendConn); err != nil { + status := errToAPIStatus(err) + writeJSON(status.Code, r.codec, status, w) + return true + } + + done := make(chan struct{}, 2) + + go func() { + _, err := io.Copy(backendConn, requestHijackedConn) + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + glog.Errorf("Error proxying data from client to backend: %v", err) } + done <- struct{}{} + }() - done := make(chan struct{}, 2) - - go func() { - _, err := io.Copy(backendConn, requestHijackedConn) - if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { - glog.Errorf("Error proxying data from client to backend: %v", err) - } - done <- struct{}{} - }() - - go func() { - _, err := io.Copy(requestHijackedConn, backendConn) - if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { - glog.Errorf("Error proxying data from backend to client: %v", err) - } - done <- struct{}{} - }() - - <-done - } else { - proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: destURL.Host}) - proxy.Transport = &proxyTransport{ - proxyScheme: req.URL.Scheme, - proxyHost: req.URL.Host, - proxyPathPrepend: path.Join(r.prefix, "ns", namespace, resource, id), + go func() { + _, err := io.Copy(requestHijackedConn, backendConn) + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + glog.Errorf("Error proxying data from backend to client: %v", err) } - proxy.FlushInterval = 200 * time.Millisecond - proxy.ServeHTTP(w, newReq) - } + done <- struct{}{} + }() + + <-done + return true } type proxyTransport struct { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/proxy_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/proxy_test.go index 90b56ec9b338..797728ffc14e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/proxy_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/proxy_test.go @@ -280,14 +280,10 @@ func TestProxy(t *testing.T) { expectedResourceNamespace: item.reqNamespace, } - namespaceHandler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper) + namespaceHandler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage}) namespaceServer := httptest.NewServer(namespaceHandler) defer namespaceServer.Close() - legacyNamespaceHandler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, legacyNamespaceMapper) + legacyNamespaceHandler := handle(map[string]RESTStorage{"foo": simpleStorage}) legacyNamespaceServer := httptest.NewServer(legacyNamespaceHandler) defer legacyNamespaceServer.Close() @@ -296,8 +292,8 @@ func TestProxy(t *testing.T) { server *httptest.Server proxyTestPattern string }{ - {namespaceServer, "/prefix/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path}, - {legacyNamespaceServer, "/prefix/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace}, + {namespaceServer, "/api/version/proxy/namespaces/" + item.reqNamespace + "/foo/id" + item.path}, + {legacyNamespaceServer, "/api/version/proxy/foo/id" + item.path + "?namespace=" + item.reqNamespace}, } for _, serverPattern := range serverPatterns { @@ -344,14 +340,12 @@ func TestProxyUpgrade(t *testing.T) { expectedResourceNamespace: "myns", } - namespaceHandler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper) + namespaceHandler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(namespaceHandler) defer server.Close() - ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/prefix/version/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/") + ws, err := websocket.Dial("ws://"+server.Listener.Addr().String()+"/api/version/proxy/namespaces/myns/foo/123", "", "http://127.0.0.1/") if err != nil { t.Fatalf("websocket dial err: %s", err) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/redirect.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/redirect.go index 5267c4cd296f..44e372ea32d2 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/redirect.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/redirect.go @@ -17,6 +17,7 @@ limitations under the License. package apiserver import ( + "fmt" "net/http" "time" @@ -38,7 +39,7 @@ func (r *RedirectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { var apiResource string var httpCode int reqStart := time.Now() - defer monitor("redirect", verb, apiResource, httpCode, reqStart) + defer monitor("redirect", &verb, &apiResource, &httpCode, reqStart) requestInfo, err := r.apiRequestInfoResolver.GetAPIRequestInfo(req) if err != nil { @@ -85,7 +86,7 @@ func (r *RedirectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - w.Header().Set("Location", location) + w.Header().Set("Location", fmt.Sprintf("http://%s", location)) w.WriteHeader(http.StatusTemporaryRedirect) httpCode = http.StatusTemporaryRedirect } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/redirect_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/redirect_test.go index 4098671f961d..c74c99994c88 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/redirect_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/redirect_test.go @@ -29,9 +29,7 @@ func TestRedirect(t *testing.T) { errors: map[string]error{}, expectedResourceNamespace: "default", } - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() @@ -54,7 +52,7 @@ func TestRedirect(t *testing.T) { for _, item := range table { simpleStorage.errors["resourceLocation"] = item.err simpleStorage.resourceLocation = item.id - resp, err := client.Get(server.URL + "/prefix/version/redirect/foo/" + item.id) + resp, err := client.Get(server.URL + "/api/version/redirect/foo/" + item.id) if resp == nil { t.Fatalf("Unexpected nil resp") } @@ -71,7 +69,7 @@ func TestRedirect(t *testing.T) { if err == nil || err.(*url.Error).Err != dontFollow { t.Errorf("Unexpected err %#v", err) } - if e, a := item.id, resp.Header.Get("Location"); e != a { + if e, a := "http://"+item.id, resp.Header.Get("Location"); e != a { t.Errorf("Expected %v, got %v", e, a) } } @@ -82,9 +80,7 @@ func TestRedirectWithNamespaces(t *testing.T) { errors: map[string]error{}, expectedResourceNamespace: "other", } - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker, admissionControl, requestContextMapper, namespaceMapper) + handler := handleNamespaced(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() @@ -107,7 +103,7 @@ func TestRedirectWithNamespaces(t *testing.T) { for _, item := range table { simpleStorage.errors["resourceLocation"] = item.err simpleStorage.resourceLocation = item.id - resp, err := client.Get(server.URL + "/prefix/version/redirect/namespaces/other/foo/" + item.id) + resp, err := client.Get(server.URL + "/api/version/redirect/namespaces/other/foo/" + item.id) if resp == nil { t.Fatalf("Unexpected nil resp") } @@ -124,7 +120,7 @@ func TestRedirectWithNamespaces(t *testing.T) { if err == nil || err.(*url.Error).Err != dontFollow { t.Errorf("Unexpected err %#v", err) } - if e, a := item.id, resp.Header.Get("Location"); e != a { + if e, a := "http://"+item.id, resp.Header.Get("Location"); e != a { t.Errorf("Expected %v, got %v", e, a) } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/resthandler.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/resthandler.go index 514f8dcce725..4c1a3d76538d 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/resthandler.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/resthandler.go @@ -17,17 +17,21 @@ limitations under the License. package apiserver import ( + "fmt" "net/http" + "net/url" gpath "path" "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/emicklei/go-restful" + "github.com/evanphx/json-patch" "github.com/golang/glog" ) @@ -60,7 +64,7 @@ func GetResource(r RESTGetter, ctxFn ContextFunc, namer ScopeNamer, codec runtim w := res.ResponseWriter namespace, name, err := namer.Name(req) if err != nil { - notFound(w, req.Request) + errorJSON(err, codec, w) return } ctx := ctxFn(req) @@ -79,25 +83,38 @@ func GetResource(r RESTGetter, ctxFn ContextFunc, namer ScopeNamer, codec runtim } } +func parseSelectorQueryParams(query url.Values, version, apiResource string) (label labels.Selector, field fields.Selector, err error) { + labelString := query.Get(api.LabelSelectorQueryParam(version)) + label, err = labels.Parse(labelString) + if err != nil { + return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'labels' selector parameter (%s) could not be parsed: %v", labelString, err)) + } + + convertToInternalVersionFunc := func(label, value string) (newLabel, newValue string, err error) { + return api.Scheme.ConvertFieldLabel(version, apiResource, label, value) + } + fieldString := query.Get(api.FieldSelectorQueryParam(version)) + field, err = fields.ParseAndTransformSelector(fieldString, convertToInternalVersionFunc) + if err != nil { + return nil, nil, errors.NewBadRequest(fmt.Sprintf("The 'fields' selector parameter (%s) could not be parsed: %v", fieldString, err)) + } + return label, field, nil +} + // ListResource returns a function that handles retrieving a list of resources from a RESTStorage object. -func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec) restful.RouteFunction { +func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, version, apiResource string) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter namespace, err := namer.Namespace(req) if err != nil { - notFound(w, req.Request) + errorJSON(err, codec, w) return } ctx := ctxFn(req) ctx = api.WithNamespace(ctx, namespace) - label, err := labels.ParseSelector(req.Request.URL.Query().Get("labels")) - if err != nil { - errorJSON(err, codec, w) - return - } - field, err := labels.ParseSelector(req.Request.URL.Query().Get("fields")) + label, field, err := parseSelectorQueryParams(req.Request.URL.Query(), version, apiResource) if err != nil { errorJSON(err, codec, w) return @@ -117,7 +134,7 @@ func ListResource(r RESTLister, ctxFn ContextFunc, namer ScopeNamer, codec runti } // CreateResource returns a function that will handle a resource creation. -func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource string, admit admission.Interface) restful.RouteFunction { +func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter @@ -126,7 +143,7 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec ru namespace, err := namer.Namespace(req) if err != nil { - notFound(w, req.Request) + errorJSON(err, codec, w) return } ctx := ctxFn(req) @@ -140,6 +157,7 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec ru obj := r.New() if err := codec.DecodeInto(body, obj); err != nil { + err = transformDecodeError(typer, err, obj, body) errorJSON(err, codec, w) return } @@ -171,8 +189,84 @@ func CreateResource(r RESTCreater, ctxFn ContextFunc, namer ScopeNamer, codec ru } } +// PatchResource returns a function that will handle a resource patch +// TODO: Eventually PatchResource should just use AtomicUpdate and this routine should be a bit cleaner +func PatchResource(r RESTPatcher, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction { + return func(req *restful.Request, res *restful.Response) { + w := res.ResponseWriter + + // TODO: we either want to remove timeout or document it (if we document, move timeout out of this function and declare it in api_installer) + timeout := parseTimeout(req.Request.URL.Query().Get("timeout")) + + namespace, name, err := namer.Name(req) + if err != nil { + errorJSON(err, codec, w) + return + } + + obj := r.New() + // PATCH requires same permission as UPDATE + err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE")) + if err != nil { + errorJSON(err, codec, w) + return + } + + ctx := ctxFn(req) + ctx = api.WithNamespace(ctx, namespace) + + original, err := r.Get(ctx, name) + if err != nil { + errorJSON(err, codec, w) + return + } + + originalObjJs, err := codec.Encode(original) + if err != nil { + errorJSON(err, codec, w) + return + } + patchJs, err := readBody(req.Request) + if err != nil { + errorJSON(err, codec, w) + return + } + patchedObjJs, err := jsonpatch.MergePatch(originalObjJs, patchJs) + if err != nil { + errorJSON(err, codec, w) + return + } + + if err := codec.DecodeInto(patchedObjJs, obj); err != nil { + errorJSON(err, codec, w) + return + } + if err := checkName(obj, name, namespace, namer); err != nil { + errorJSON(err, codec, w) + return + } + + result, err := finishRequest(timeout, func() (runtime.Object, error) { + // update should never create as previous get would fail + obj, _, err := r.Update(ctx, obj) + return obj, err + }) + if err != nil { + errorJSON(err, codec, w) + return + } + + if err := setSelfLink(result, req, namer); err != nil { + errorJSON(err, codec, w) + return + } + + writeJSON(http.StatusOK, codec, result, w) + } +} + // UpdateResource returns a function that will handle a resource update -func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource string, admit admission.Interface) restful.RouteFunction { +func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, typer runtime.ObjectTyper, resource string, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter @@ -181,7 +275,7 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec ru namespace, name, err := namer.Name(req) if err != nil { - notFound(w, req.Request) + errorJSON(err, codec, w) return } ctx := ctxFn(req) @@ -195,22 +289,14 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec ru obj := r.New() if err := codec.DecodeInto(body, obj); err != nil { + err = transformDecodeError(typer, err, obj, body) errorJSON(err, codec, w) return } - // check the provided name against the request - if objNamespace, objName, err := namer.ObjectName(obj); err == nil { - if objName != name { - errorJSON(errors.NewBadRequest("the name of the object does not match the name on the URL"), codec, w) - return - } - if len(namespace) > 0 { - if len(objNamespace) > 0 && objNamespace != namespace { - errorJSON(errors.NewBadRequest("the namespace of the object does not match the namespace on the request"), codec, w) - return - } - } + if err := checkName(obj, name, namespace, namer); err != nil { + errorJSON(err, codec, w) + return } err = admit.Admit(admission.NewAttributesRecord(obj, namespace, resource, "UPDATE")) @@ -244,7 +330,7 @@ func UpdateResource(r RESTUpdater, ctxFn ContextFunc, namer ScopeNamer, codec ru } // DeleteResource returns a function that will handle a resource deletion -func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource, kind string, admit admission.Interface) restful.RouteFunction { +func DeleteResource(r RESTGracefulDeleter, checkBody bool, ctxFn ContextFunc, namer ScopeNamer, codec runtime.Codec, resource, kind string, admit admission.Interface) restful.RouteFunction { return func(req *restful.Request, res *restful.Response) { w := res.ResponseWriter @@ -253,7 +339,7 @@ func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec ru namespace, name, err := namer.Name(req) if err != nil { - notFound(w, req.Request) + errorJSON(err, codec, w) return } ctx := ctxFn(req) @@ -261,6 +347,21 @@ func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec ru ctx = api.WithNamespace(ctx, namespace) } + options := &api.DeleteOptions{} + if checkBody { + body, err := readBody(req.Request) + if err != nil { + errorJSON(err, codec, w) + return + } + if len(body) > 0 { + if err := codec.DecodeInto(body, options); err != nil { + errorJSON(err, codec, w) + return + } + } + } + err = admit.Admit(admission.NewAttributesRecord(nil, namespace, resource, "DELETE")) if err != nil { errorJSON(err, codec, w) @@ -268,7 +369,7 @@ func DeleteResource(r RESTDeleter, ctxFn ContextFunc, namer ScopeNamer, codec ru } result, err := finishRequest(timeout, func() (runtime.Object, error) { - return r.Delete(ctx, name) + return r.Delete(ctx, name, options) }) if err != nil { errorJSON(err, codec, w) @@ -305,8 +406,10 @@ type resultFunc func() (runtime.Object, error) // finishRequest makes a given resultFunc asynchronous and handles errors returned by the response. // Any api.Status object returned is considered an "error", which interrupts the normal response flow. func finishRequest(timeout time.Duration, fn resultFunc) (result runtime.Object, err error) { - ch := make(chan runtime.Object) - errCh := make(chan error) + // these channels need to be buffered to prevent the goroutine below from hanging indefinitely + // when the select statement reads something other than the one the goroutine sends on. + ch := make(chan runtime.Object, 1) + errCh := make(chan error, 1) go func() { if result, err := fn(); err != nil { errCh <- err @@ -328,6 +431,18 @@ func finishRequest(timeout time.Duration, fn resultFunc) (result runtime.Object, } } +// transformDecodeError adds additional information when a decode fails. +func transformDecodeError(typer runtime.ObjectTyper, baseErr error, into runtime.Object, body []byte) error { + _, kind, err := typer.ObjectVersionAndKind(into) + if err != nil { + return err + } + if version, dataKind, err := typer.DataVersionAndKind(body); err == nil && len(dataKind) > 0 { + return errors.NewBadRequest(fmt.Sprintf("%s in version %s cannot be handled as a %s: %v", dataKind, version, kind, baseErr)) + } + return errors.NewBadRequest(fmt.Sprintf("the object provided is unrecognized (must be of type %s): %v", kind, baseErr)) +} + // setSelfLink sets the self link of an object (or the child items in a list) to the base URL of the request // plus the path and query generated by the provided linkFunc func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error { @@ -350,6 +465,21 @@ func setSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) err return namer.SetSelfLink(obj, newURL.String()) } +// checkName checks the provided name against the request +func checkName(obj runtime.Object, name, namespace string, namer ScopeNamer) error { + if objNamespace, objName, err := namer.ObjectName(obj); err == nil { + if objName != name { + return errors.NewBadRequest("the name of the object does not match the name on the URL") + } + if len(namespace) > 0 { + if len(objNamespace) > 0 && objNamespace != namespace { + return errors.NewBadRequest("the namespace of the object does not match the namespace on the request") + } + } + } + return nil +} + // setListSelfLink sets the self link of a list to the base URL, then sets the self links // on all child objects returned. func setListSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) error { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/validator.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/validator.go index f4c2bf910468..037204a540fd 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/validator.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/validator.go @@ -73,9 +73,11 @@ type ServerStatus struct { } func (v *validator) ServeHTTP(w http.ResponseWriter, r *http.Request) { + verb := "get" + apiResource := "" var httpCode int reqStart := time.Now() - defer monitor("validate", "get", "", httpCode, reqStart) + defer monitor("validate", &verb, &apiResource, &httpCode, reqStart) reply := []ServerStatus{} for name, server := range v.servers() { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/watch.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/watch.go index e7698182c978..c98228316f12 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/watch.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/watch.go @@ -17,8 +17,8 @@ limitations under the License. package apiserver import ( + "fmt" "net/http" - "net/url" "path" "regexp" "strings" @@ -27,7 +27,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" @@ -56,25 +55,6 @@ func (h *WatchHandler) setSelfLinkAddName(obj runtime.Object, req *http.Request) return h.linker.SetSelfLink(obj, newURL.String()) } -func getWatchParams(query url.Values) (label, field labels.Selector, resourceVersion string, err error) { - s, perr := labels.ParseSelector(query.Get("labels")) - if perr != nil { - err = perr - return - } - label = s - - s, perr = labels.ParseSelector(query.Get("fields")) - if perr != nil { - err = perr - return - } - field = s - - resourceVersion = query.Get("resourceVersion") - return -} - var connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)") func isWebsocketRequest(req *http.Request) bool { @@ -87,18 +67,18 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { var apiResource string var httpCode int reqStart := time.Now() - defer monitor("watch", verb, apiResource, httpCode, reqStart) + defer monitor("watch", &verb, &apiResource, &httpCode, reqStart) if req.Method != "GET" { - notFound(w, req) - httpCode = http.StatusNotFound + httpCode = errorJSON(errors.NewBadRequest( + fmt.Sprintf("unsupported method for watch: %s", req.Method)), h.codec, w) return } requestInfo, err := h.info.GetAPIRequestInfo(req) if err != nil { - notFound(w, req) - httpCode = http.StatusNotFound + httpCode = errorJSON(errors.NewBadRequest( + fmt.Sprintf("failed to find api request info: %s", err.Error())), h.codec, w) return } verb = requestInfo.Verb @@ -106,8 +86,7 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { storage := h.storage[requestInfo.Resource] if storage == nil { - notFound(w, req) - httpCode = http.StatusNotFound + httpCode = errorJSON(errors.NewNotFound(requestInfo.Resource, "Resource"), h.codec, w) return } apiResource = requestInfo.Resource @@ -117,11 +96,13 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - label, field, resourceVersion, err := getWatchParams(req.URL.Query()) + label, field, err := parseSelectorQueryParams(req.URL.Query(), requestInfo.APIVersion, apiResource) if err != nil { httpCode = errorJSON(err, h.codec, w) return } + + resourceVersion := req.URL.Query().Get("resourceVersion") watching, err := watcher.Watch(ctx, label, field, resourceVersion) if err != nil { httpCode = errorJSON(err, h.codec, w) diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/watch_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/watch_test.go index d4a702addc82..07938da0ae4e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/watch_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver/watch_test.go @@ -25,6 +25,8 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "golang.org/x/net/websocket" @@ -48,9 +50,7 @@ var watchTestTable = []struct { func TestWatchWebsocket(t *testing.T) { simpleStorage := &SimpleRESTStorage{} _ = ResourceWatcher(simpleStorage) // Give compile error if this doesn't work. - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() @@ -102,9 +102,7 @@ func TestWatchWebsocket(t *testing.T) { func TestWatchHTTP(t *testing.T) { simpleStorage := &SimpleRESTStorage{} - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -164,15 +162,17 @@ func TestWatchHTTP(t *testing.T) { } func TestWatchParamParsing(t *testing.T) { + api.Scheme.AddFieldLabelConversionFunc(testVersion, "foo", + func(label, value string) (string, string, error) { + return label, value, nil + }) simpleStorage := &SimpleRESTStorage{} - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() dest, _ := url.Parse(server.URL) - dest.Path = "/api/version/watch/foo" + dest.Path = "/api/" + testVersion + "/watch/foo" table := []struct { rawQuery string @@ -188,16 +188,16 @@ func TestWatchParamParsing(t *testing.T) { fieldSelector: "", namespace: api.NamespaceAll, }, { - rawQuery: "namespace=default&resourceVersion=314159&fields=Host%3D&labels=name%3Dfoo", + rawQuery: "namespace=default&resourceVersion=314159&" + api.FieldSelectorQueryParam(testVersion) + "=Host%3D&" + api.LabelSelectorQueryParam(testVersion) + "=name%3Dfoo", resourceVersion: "314159", labelSelector: "name=foo", fieldSelector: "Host=", namespace: api.NamespaceDefault, }, { - rawQuery: "namespace=watchother&fields=ID%3dfoo&resourceVersion=1492", + rawQuery: "namespace=watchother&" + api.FieldSelectorQueryParam(testVersion) + "=id%3dfoo&resourceVersion=1492", resourceVersion: "1492", labelSelector: "", - fieldSelector: "ID=foo", + fieldSelector: "id=foo", namespace: "watchother", }, { rawQuery: "", @@ -209,8 +209,8 @@ func TestWatchParamParsing(t *testing.T) { } for _, item := range table { - simpleStorage.requestedLabelSelector = nil - simpleStorage.requestedFieldSelector = nil + simpleStorage.requestedLabelSelector = labels.Everything() + simpleStorage.requestedFieldSelector = fields.Everything() simpleStorage.requestedResourceVersion = "5" // Prove this is set in all cases simpleStorage.requestedResourceNamespace = "" dest.RawQuery = item.rawQuery @@ -237,9 +237,7 @@ func TestWatchParamParsing(t *testing.T) { func TestWatchProtocolSelection(t *testing.T) { simpleStorage := &SimpleRESTStorage{} - handler := Handle(map[string]RESTStorage{ - "foo": simpleStorage, - }, codec, "/api", "version", selfLinker, admissionControl, requestContextMapper, mapper) + handler := handle(map[string]RESTStorage{"foo": simpleStorage}) server := httptest.NewServer(handler) defer server.Close() defer server.CloseClientConnections() diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer/abac/abac_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer/abac/abac_test.go index 40cfff0ebfb8..88959831e433 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer/abac/abac_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer/abac/abac_test.go @@ -266,3 +266,81 @@ func newWithContents(t *testing.T, contents string) (authorizer.Authorizer, erro pl, err := NewFromFile(f.Name()) return pl, err } + +func TestPolicy(t *testing.T) { + tests := []struct { + policy policy + attr authorizer.Attributes + matches bool + name string + }{ + { + policy: policy{}, + attr: authorizer.AttributesRecord{}, + matches: true, + name: "null", + }, + { + policy: policy{ + Readonly: true, + }, + attr: authorizer.AttributesRecord{}, + matches: false, + name: "read-only mismatch", + }, + { + policy: policy{ + User: "foo", + }, + attr: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Name: "bar", + }, + }, + matches: false, + name: "user name mis-match", + }, + { + policy: policy{ + Resource: "foo", + }, + attr: authorizer.AttributesRecord{ + Resource: "bar", + }, + matches: false, + name: "resource mis-match", + }, + { + policy: policy{ + User: "foo", + Resource: "foo", + Namespace: "foo", + }, + attr: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Name: "foo", + }, + Resource: "foo", + Namespace: "foo", + }, + matches: true, + name: "namespace mis-match", + }, + { + policy: policy{ + Namespace: "foo", + }, + attr: authorizer.AttributesRecord{ + Namespace: "bar", + }, + matches: false, + name: "resource mis-match", + }, + } + for _, test := range tests { + matches := test.policy.matches(test.attr) + if test.matches != matches { + t.Errorf("unexpected value for %s, expected: %s, saw: %s", test.name, test.matches, matches) + } + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listers.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listers.go index b58a79e16cc1..f989116fad85 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listers.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listers.go @@ -52,6 +52,15 @@ func (s *StoreToPodLister) List(selector labels.Selector) (pods []api.Pod, err e return pods, nil } +// Exists returns true if a pod matching the namespace/name of the given pod exists in the store. +func (s *StoreToPodLister) Exists(pod *api.Pod) (bool, error) { + _, exists, err := s.Store.Get(pod) + if err != nil { + return false, err + } + return exists, nil +} + // StoreToNodeLister makes a Store have the List method of the client.NodeInterface // The Store must contain (only) Nodes. type StoreToNodeLister struct { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listers_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listers_test.go index eded9f5f676d..3f759c3c715b 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listers_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listers_test.go @@ -72,5 +72,21 @@ func TestStoreToPodLister(t *testing.T) { t.Errorf("Expected %v, got %v", e, a) continue } + + exists, err := spl.Exists(&api.Pod{ObjectMeta: api.ObjectMeta{Name: id}}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !exists { + t.Errorf("exists returned false for %v", id) + } + } + + exists, err := spl.Exists(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "qux"}}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if exists { + t.Errorf("Unexpected pod exists") } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listwatch.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listwatch.go index 96fdecaa0401..013571d562a6 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listwatch.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listwatch.go @@ -17,8 +17,9 @@ limitations under the License. package cache import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -37,13 +38,13 @@ type ListWatch struct { WatchFunc WatchFunc } -// ListWatchFromClient creates a new ListWatch from the specified client, resource, namespace and field selector -func NewListWatchFromClient(client *client.Client, resource string, namespace string, fieldSelector labels.Selector) *ListWatch { +// NewListWatchFromClient creates a new ListWatch from the specified client, resource, namespace and field selector. +func NewListWatchFromClient(c *client.Client, resource string, namespace string, fieldSelector fields.Selector) *ListWatch { listFunc := func() (runtime.Object, error) { - return client.Get().Namespace(namespace).Resource(resource).SelectorParam("fields", fieldSelector).Do().Get() + return c.Get().Namespace(namespace).Resource(resource).FieldsSelectorParam(api.FieldSelectorQueryParam(c.APIVersion()), fieldSelector).Do().Get() } watchFunc := func(resourceVersion string) (watch.Interface, error) { - return client.Get().Prefix("watch").Namespace(namespace).Resource(resource).SelectorParam("fields", fieldSelector).Param("resourceVersion", resourceVersion).Watch() + return c.Get().Prefix("watch").Namespace(namespace).Resource(resource).FieldsSelectorParam(api.FieldSelectorQueryParam(c.APIVersion()), fieldSelector).Param("resourceVersion", resourceVersion).Watch() } return &ListWatch{ListFunc: listFunc, WatchFunc: watchFunc} } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listwatch_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listwatch_test.go index 43529761d9dd..6ddc9b310787 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listwatch_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/listwatch_test.go @@ -25,12 +25,12 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) -func parseSelectorOrDie(s string) labels.Selector { - selector, err := labels.ParseSelector(s) +func parseSelectorOrDie(s string) fields.Selector { + selector, err := fields.ParseSelector(s) if err != nil { panic(err) } @@ -48,6 +48,15 @@ func buildResourcePath(prefix, namespace, resource string) string { return path.Join(base, resource) } +func getHostFieldLabel() string { + switch testapi.Version() { + case "v1beta1", "v1beta2": + return "DesiredState.Host" + default: + return "spec.host" + } +} + // buildQueryValues is a convenience function for knowing if a namespace should be in a query param or not func buildQueryValues(namespace string, query url.Values) url.Values { v := url.Values{} @@ -73,7 +82,7 @@ func TestListWatchesCanList(t *testing.T) { location string resource string namespace string - fieldSelector labels.Selector + fieldSelector fields.Selector }{ // Minion { @@ -84,17 +93,17 @@ func TestListWatchesCanList(t *testing.T) { }, // pod with "assigned" field selector. { - location: buildLocation(buildResourcePath("", api.NamespaceAll, "pods"), buildQueryValues(api.NamespaceAll, url.Values{"fields": []string{"DesiredState.Host="}})), + location: buildLocation(buildResourcePath("", api.NamespaceAll, "pods"), buildQueryValues(api.NamespaceAll, url.Values{"fields": []string{getHostFieldLabel() + "="}})), resource: "pods", namespace: api.NamespaceAll, - fieldSelector: labels.Set{"DesiredState.Host": ""}.AsSelector(), + fieldSelector: fields.Set{getHostFieldLabel(): ""}.AsSelector(), }, // pod in namespace "foo" { - location: buildLocation(buildResourcePath("", "foo", "pods"), buildQueryValues("foo", url.Values{"fields": []string{"DesiredState.Host="}})), + location: buildLocation(buildResourcePath("", "foo", "pods"), buildQueryValues("foo", url.Values{"fields": []string{getHostFieldLabel() + "="}})), resource: "pods", namespace: "foo", - fieldSelector: labels.Set{"DesiredState.Host": ""}.AsSelector(), + fieldSelector: fields.Set{getHostFieldLabel(): ""}.AsSelector(), }, } for _, item := range table { @@ -119,7 +128,7 @@ func TestListWatchesCanWatch(t *testing.T) { location string resource string namespace string - fieldSelector labels.Selector + fieldSelector fields.Selector }{ // Minion { @@ -138,19 +147,19 @@ func TestListWatchesCanWatch(t *testing.T) { }, // pod with "assigned" field selector. { - location: buildLocation(buildResourcePath("watch", api.NamespaceAll, "pods"), buildQueryValues(api.NamespaceAll, url.Values{"fields": []string{"DesiredState.Host="}, "resourceVersion": []string{"0"}})), + location: buildLocation(buildResourcePath("watch", api.NamespaceAll, "pods"), buildQueryValues(api.NamespaceAll, url.Values{"fields": []string{getHostFieldLabel() + "="}, "resourceVersion": []string{"0"}})), rv: "0", resource: "pods", namespace: api.NamespaceAll, - fieldSelector: labels.Set{"DesiredState.Host": ""}.AsSelector(), + fieldSelector: fields.Set{getHostFieldLabel(): ""}.AsSelector(), }, // pod with namespace foo and assigned field selector { - location: buildLocation(buildResourcePath("watch", "foo", "pods"), buildQueryValues("foo", url.Values{"fields": []string{"DesiredState.Host="}, "resourceVersion": []string{"0"}})), + location: buildLocation(buildResourcePath("watch", "foo", "pods"), buildQueryValues("foo", url.Values{"fields": []string{getHostFieldLabel() + "="}, "resourceVersion": []string{"0"}})), rv: "0", resource: "pods", namespace: "foo", - fieldSelector: labels.Set{"DesiredState.Host": ""}.AsSelector(), + fieldSelector: fields.Set{getHostFieldLabel(): ""}.AsSelector(), }, } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/poller_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/poller_test.go index 2c8b240b71b9..2c756b31275d 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/poller_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/poller_test.go @@ -104,6 +104,8 @@ func TestPoller_sync(t *testing.T) { } func TestPoller_Run(t *testing.T) { + stopCh := make(chan struct{}) + defer func() { stopCh <- struct{}{} }() s := NewStore(testPairKeyFunc) const count = 10 var called = 0 @@ -118,7 +120,7 @@ func TestPoller_Run(t *testing.T) { return testEnumerator{}, nil } return nil, errors.New("transient error") - }, time.Millisecond, s).Run() + }, time.Millisecond, s).RunUntil(stopCh) // The test here is that we get called at least count times. <-done diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/reflector.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/reflector.go index f484b3a2bafe..b7960417a4ed 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/reflector.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/reflector.go @@ -49,18 +49,31 @@ type Reflector struct { listerWatcher ListerWatcher // period controls timing between one watch ending and // the beginning of the next one. - period time.Duration + period time.Duration + resyncPeriod time.Duration +} + +// NewNamespaceKeyedIndexerAndReflector creates an Indexer and a Reflector +// The indexer is configured to key on namespace +func NewNamespaceKeyedIndexerAndReflector(lw ListerWatcher, expectedType interface{}, resyncPeriod time.Duration) (indexer Indexer, reflector *Reflector) { + indexer = NewIndexer(MetaNamespaceKeyFunc, Indexers{"namespace": MetaNamespaceIndexFunc}) + reflector = NewReflector(lw, expectedType, indexer, resyncPeriod) + return indexer, reflector } // NewReflector creates a new Reflector object which will keep the given store up to // date with the server's contents for the given resource. Reflector promises to // only put things in the store that have the type of expectedType. -func NewReflector(lw ListerWatcher, expectedType interface{}, store Store) *Reflector { +// If resyncPeriod is non-zero, then lists will be executed after every resyncPeriod, +// so that you can use reflectors to periodically process everything as well as +// incrementally processing the things that change. +func NewReflector(lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector { r := &Reflector{ listerWatcher: lw, store: store, expectedType: reflect.TypeOf(expectedType), period: time.Second, + resyncPeriod: resyncPeriod, } return r } @@ -77,8 +90,25 @@ func (r *Reflector) RunUntil(stopCh <-chan struct{}) { go util.Until(func() { r.listAndWatch() }, r.period, stopCh) } +var ( + // nothing will ever be sent down this channel + neverExitWatch <-chan time.Time = make(chan time.Time) + + // Used to indicate that watching stopped so that a resync could happen. + errorResyncRequested = errors.New("resync channel fired") +) + +// resyncChan returns a channel which will receive something when a resync is required. +func (r *Reflector) resyncChan() <-chan time.Time { + if r.resyncPeriod == 0 { + return neverExitWatch + } + return time.After(r.resyncPeriod) +} + func (r *Reflector) listAndWatch() { var resourceVersion string + exitWatch := r.resyncChan() list, err := r.listerWatcher.List() if err != nil { @@ -114,8 +144,10 @@ func (r *Reflector) listAndWatch() { } return } - if err := r.watchHandler(w, &resourceVersion); err != nil { - glog.Errorf("watch of %v ended with error: %v", r.expectedType, err) + if err := r.watchHandler(w, &resourceVersion, exitWatch); err != nil { + if err != errorResyncRequested { + glog.Errorf("watch of %v ended with error: %v", r.expectedType, err) + } return } } @@ -132,41 +164,47 @@ func (r *Reflector) syncWith(items []runtime.Object) error { } // watchHandler watches w and keeps *resourceVersion up to date. -func (r *Reflector) watchHandler(w watch.Interface, resourceVersion *string) error { +func (r *Reflector) watchHandler(w watch.Interface, resourceVersion *string, exitWatch <-chan time.Time) error { start := time.Now() eventCount := 0 +loop: for { - event, ok := <-w.ResultChan() - if !ok { - break - } - if event.Type == watch.Error { - return apierrs.FromObject(event.Object) - } - if e, a := r.expectedType, reflect.TypeOf(event.Object); e != a { - glog.Errorf("expected type %v, but watch event object had type %v", e, a) - continue - } - meta, err := meta.Accessor(event.Object) - if err != nil { - glog.Errorf("unable to understand watch event %#v", event) - continue - } - switch event.Type { - case watch.Added: - r.store.Add(event.Object) - case watch.Modified: - r.store.Update(event.Object) - case watch.Deleted: - // TODO: Will any consumers need access to the "last known - // state", which is passed in event.Object? If so, may need - // to change this. - r.store.Delete(event.Object) - default: - glog.Errorf("unable to understand watch event %#v", event) + select { + case <-exitWatch: + w.Stop() + return errorResyncRequested + case event, ok := <-w.ResultChan(): + if !ok { + break loop + } + if event.Type == watch.Error { + return apierrs.FromObject(event.Object) + } + if e, a := r.expectedType, reflect.TypeOf(event.Object); e != a { + glog.Errorf("expected type %v, but watch event object had type %v", e, a) + continue + } + meta, err := meta.Accessor(event.Object) + if err != nil { + glog.Errorf("unable to understand watch event %#v", event) + continue + } + switch event.Type { + case watch.Added: + r.store.Add(event.Object) + case watch.Modified: + r.store.Update(event.Object) + case watch.Deleted: + // TODO: Will any consumers need access to the "last known + // state", which is passed in event.Object? If so, may need + // to change this. + r.store.Delete(event.Object) + default: + glog.Errorf("unable to understand watch event %#v", event) + } + *resourceVersion = meta.ResourceVersion() + eventCount++ } - *resourceVersion = meta.ResourceVersion() - eventCount++ } watchDuration := time.Now().Sub(start) diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/reflector_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/reflector_test.go index 38f5a6eaaae9..5996329bd322 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/reflector_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache/reflector_test.go @@ -20,6 +20,7 @@ import ( "fmt" "strconv" "testing" + "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -36,15 +37,27 @@ func (t *testLW) Watch(resourceVersion string) (watch.Interface, error) { return t.WatchFunc(resourceVersion) } +func TestReflector_resyncChan(t *testing.T) { + s := NewStore(MetaNamespaceKeyFunc) + g := NewReflector(&testLW{}, &api.Pod{}, s, time.Millisecond) + a, b := g.resyncChan(), time.After(100*time.Millisecond) + select { + case <-a: + t.Logf("got timeout as expected") + case <-b: + t.Errorf("resyncChan() is at least 99 milliseconds late??") + } +} + func TestReflector_watchHandlerError(t *testing.T) { s := NewStore(MetaNamespaceKeyFunc) - g := NewReflector(&testLW{}, &api.Pod{}, s) + g := NewReflector(&testLW{}, &api.Pod{}, s, 0) fw := watch.NewFake() go func() { fw.Stop() }() var resumeRV string - err := g.watchHandler(fw, &resumeRV) + err := g.watchHandler(fw, &resumeRV, neverExitWatch) if err == nil { t.Errorf("unexpected non-error") } @@ -52,7 +65,7 @@ func TestReflector_watchHandlerError(t *testing.T) { func TestReflector_watchHandler(t *testing.T) { s := NewStore(MetaNamespaceKeyFunc) - g := NewReflector(&testLW{}, &api.Pod{}, s) + g := NewReflector(&testLW{}, &api.Pod{}, s, 0) fw := watch.NewFake() s.Add(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}) s.Add(&api.Pod{ObjectMeta: api.ObjectMeta{Name: "bar"}}) @@ -64,7 +77,7 @@ func TestReflector_watchHandler(t *testing.T) { fw.Stop() }() var resumeRV string - err := g.watchHandler(fw, &resumeRV) + err := g.watchHandler(fw, &resumeRV, neverExitWatch) if err != nil { t.Errorf("unexpected error %v", err) } @@ -101,6 +114,19 @@ func TestReflector_watchHandler(t *testing.T) { } } +func TestReflector_watchHandlerTimeout(t *testing.T) { + s := NewStore(MetaNamespaceKeyFunc) + g := NewReflector(&testLW{}, &api.Pod{}, s, 0) + fw := watch.NewFake() + var resumeRV string + exit := make(chan time.Time, 1) + exit <- time.Now() + err := g.watchHandler(fw, &resumeRV, exit) + if err != errorResyncRequested { + t.Errorf("expected timeout error, but got %q", err) + } +} + func TestReflector_listAndWatch(t *testing.T) { createdFakes := make(chan *watch.FakeWatcher) @@ -125,7 +151,7 @@ func TestReflector_listAndWatch(t *testing.T) { }, } s := NewFIFO(MetaNamespaceKeyFunc) - r := NewReflector(lw, &api.Pod{}, s) + r := NewReflector(lw, &api.Pod{}, s, 0) go r.listAndWatch() ids := []string{"foo", "bar", "baz", "qux", "zoo"} @@ -242,7 +268,7 @@ func TestReflector_listAndWatchWithErrors(t *testing.T) { return item.list, item.listErr }, } - r := NewReflector(lw, &api.Pod{}, s) + r := NewReflector(lw, &api.Pod{}, s, 0) r.listAndWatch() } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/client.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/client.go index d12bebd741f3..debd24cc6b3e 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/client.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/client.go @@ -39,7 +39,6 @@ type Interface interface { EventNamespacer LimitRangesNamespacer ResourceQuotasNamespacer - ResourceQuotaUsagesNamespacer SecretsNamespacer NamespacesInterface } @@ -76,10 +75,6 @@ func (c *Client) ResourceQuotas(namespace string) ResourceQuotaInterface { return newResourceQuotas(c, namespace) } -func (c *Client) ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterface { - return newResourceQuotaUsages(c, namespace) -} - func (c *Client) Secrets(namespace string) SecretsInterface { return newSecrets(c, namespace) } @@ -154,8 +149,3 @@ func IsTimeout(err error) bool { } return false } - -// preV1Beta3 returns true if the provided API version is an API introduced before v1beta3. -func preV1Beta3(version string) bool { - return version == "v1beta1" || version == "v1beta2" -} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/client_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/client_test.go index 096b887dbcf6..7cc8fb617b97 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/client_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/client_test.go @@ -30,6 +30,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -139,8 +140,10 @@ func (c *testClient) ValidateCommon(t *testing.T, err error) { validator, ok := c.QueryValidator[key] if !ok { switch key { - case "labels", "fields": + case "labels": validator = validateLabels + case "fields": + validator = validateFields default: validator = func(a, b string) bool { return a == b } } @@ -227,8 +230,20 @@ func TestListPods(t *testing.T) { } func validateLabels(a, b string) bool { - sA, _ := labels.ParseSelector(a) - sB, _ := labels.ParseSelector(b) + sA, eA := labels.Parse(a) + if eA != nil { + return false + } + sB, eB := labels.Parse(b) + if eB != nil { + return false + } + return sA.String() == sB.String() +} + +func validateFields(a, b string) bool { + sA, _ := fields.ParseSelector(a) + sB, _ := fields.ParseSelector(b) return sA.String() == sB.String() } @@ -730,14 +745,12 @@ func TestCreateMinion(t *testing.T) { ObjectMeta: api.ObjectMeta{ Name: "minion-1", }, - Status: api.NodeStatus{ - HostIP: "123.321.456.654", - }, Spec: api.NodeSpec{ Capacity: api.ResourceList{ api.ResourceCPU: resource.MustParse("1000m"), api.ResourceMemory: resource.MustParse("1Mi"), }, + Unschedulable: false, }, } c := &testClient{ @@ -771,6 +784,7 @@ func TestUpdateMinion(t *testing.T) { api.ResourceCPU: resource.MustParse("1000m"), api.ResourceMemory: resource.MustParse("1Mi"), }, + Unschedulable: true, }, } c := &testClient{ diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/auth_loaders.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/auth_loaders.go index e3c63962e817..cd18d93bbeb2 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/auth_loaders.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/auth_loaders.go @@ -80,7 +80,7 @@ func promptForString(field string, r io.Reader) string { return result } -// NewDefaultAuthLoader is an AuthLoader that parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. +// NewPromptingAuthLoader is an AuthLoader that parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. func NewPromptingAuthLoader(reader io.Reader) *PromptingAuthLoader { return &PromptingAuthLoader{reader} } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/loader_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/loader_test.go index 17e91c75b586..d9f30ba26f31 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/loader_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/loader_test.go @@ -78,7 +78,7 @@ var ( func TestNonExistentCommandLineFile(t *testing.T) { loadingRules := ClientConfigLoadingRules{ - CommandLinePath: "bogus_file", + ExplicitPath: "bogus_file", } _, err := loadingRules.Load() @@ -92,9 +92,7 @@ func TestNonExistentCommandLineFile(t *testing.T) { func TestToleratingMissingFiles(t *testing.T) { loadingRules := ClientConfigLoadingRules{ - EnvVarPath: "bogus1", - CurrentDirectoryPath: "bogus2", - HomeDirectoryPath: "bogus3", + Precedence: []string{"bogus1", "bogus2", "bogus3"}, } _, err := loadingRules.Load() @@ -112,7 +110,7 @@ func TestErrorReadingFile(t *testing.T) { } loadingRules := ClientConfigLoadingRules{ - CommandLinePath: commandLineFile.Name(), + ExplicitPath: commandLineFile.Name(), } _, err := loadingRules.Load() @@ -132,7 +130,7 @@ func TestErrorReadingNonFile(t *testing.T) { defer os.Remove(tmpdir) loadingRules := ClientConfigLoadingRules{ - CommandLinePath: tmpdir, + ExplicitPath: tmpdir, } _, err = loadingRules.Load() @@ -161,8 +159,8 @@ func TestConflictingCurrentContext(t *testing.T) { WriteToFile(mockEnvVarConfig, envVarFile.Name()) loadingRules := ClientConfigLoadingRules{ - CommandLinePath: commandLineFile.Name(), - EnvVarPath: envVarFile.Name(), + ExplicitPath: commandLineFile.Name(), + Precedence: []string{envVarFile.Name()}, } mergedConfig, err := loadingRules.Load() @@ -211,8 +209,8 @@ func TestResolveRelativePaths(t *testing.T) { WriteToFile(pathResolutionConfig2, configFile2) loadingRules := ClientConfigLoadingRules{ - CommandLinePath: configFile1, - EnvVarPath: configFile2, + ExplicitPath: configFile1, + Precedence: []string{configFile2}, } mergedConfig, err := loadingRules.Load() @@ -286,8 +284,8 @@ func ExampleMergingSomeWithConflict() { WriteToFile(testConfigConflictAlfa, envVarFile.Name()) loadingRules := ClientConfigLoadingRules{ - CommandLinePath: commandLineFile.Name(), - EnvVarPath: envVarFile.Name(), + ExplicitPath: commandLineFile.Name(), + Precedence: []string{envVarFile.Name()}, } mergedConfig, err := loadingRules.Load() @@ -346,10 +344,8 @@ func ExampleMergingEverythingNoConflicts() { WriteToFile(testConfigDelta, homeDirFile.Name()) loadingRules := ClientConfigLoadingRules{ - CommandLinePath: commandLineFile.Name(), - EnvVarPath: envVarFile.Name(), - CurrentDirectoryPath: currentDirFile.Name(), - HomeDirectoryPath: homeDirFile.Name(), + ExplicitPath: commandLineFile.Name(), + Precedence: []string{envVarFile.Name(), currentDirFile.Name(), homeDirFile.Name()}, } mergedConfig, err := loadingRules.Load() diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/merged_client_builder_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/merged_client_builder_test.go index 2c659ebfc7b5..266bd3d386a1 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/merged_client_builder_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/merged_client_builder_test.go @@ -78,11 +78,11 @@ func testWriteAuthInfoFile(auth clientauth.Info, filename string) error { } func testBindClientConfig(cmd *cobra.Command) ClientConfig { - loadingRules := NewClientConfigLoadingRules() - loadingRules.EnvVarPath = "" - loadingRules.HomeDirectoryPath = "" - loadingRules.CurrentDirectoryPath = "" - cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + loadingRules := NewDefaultClientConfigLoadingRules() + loadingRules.Precedence[DefaultEnvVarIndex] = "" + loadingRules.Precedence[DefaultCurrentDirIndex] = "" + loadingRules.Precedence[DefaultHomeDirIndex] = "" + cmd.PersistentFlags().StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") overrides := &ConfigOverrides{} BindOverrideFlags(overrides, cmd.PersistentFlags(), RecommendedConfigOverrideFlags("")) diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/overrides.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/overrides.go index 0b2e01f3bef1..160444bf1ab3 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/overrides.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/overrides.go @@ -81,6 +81,7 @@ const ( FlagCertFile = "client-certificate" FlagKeyFile = "client-key" FlagCAFile = "certificate-authority" + FlagEmbedCerts = "embed-certs" FlagBearerToken = "token" FlagUsername = "username" FlagPassword = "password" diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/containerinfo.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/containerinfo.go index a6dc2f681e5b..3efd258206dc 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/containerinfo.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/containerinfo.go @@ -25,16 +25,16 @@ import ( "net/http" "strconv" - "github.com/google/cadvisor/info" + cadvisorApi "github.com/google/cadvisor/info/v1" ) type ContainerInfoGetter interface { // GetContainerInfo returns information about a container. - GetContainerInfo(host, podID, containerID string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) + GetContainerInfo(host, podID, containerID string, req *cadvisorApi.ContainerInfoRequest) (*cadvisorApi.ContainerInfo, error) // GetRootInfo returns information about the root container on a machine. - GetRootInfo(host string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) + GetRootInfo(host string, req *cadvisorApi.ContainerInfoRequest) (*cadvisorApi.ContainerInfo, error) // GetMachineInfo returns the machine's information like number of cores, memory capacity. - GetMachineInfo(host string) (*info.MachineInfo, error) + GetMachineInfo(host string) (*cadvisorApi.MachineInfo, error) } type HTTPContainerInfoGetter struct { @@ -42,7 +42,7 @@ type HTTPContainerInfoGetter struct { Port int } -func (self *HTTPContainerInfoGetter) GetMachineInfo(host string) (*info.MachineInfo, error) { +func (self *HTTPContainerInfoGetter) GetMachineInfo(host string) (*cadvisorApi.MachineInfo, error) { request, err := http.NewRequest( "GET", fmt.Sprintf("http://%v/spec", @@ -63,7 +63,7 @@ func (self *HTTPContainerInfoGetter) GetMachineInfo(host string) (*info.MachineI return nil, fmt.Errorf("trying to get machine spec from %v; received status %v", host, response.Status) } - var minfo info.MachineInfo + var minfo cadvisorApi.MachineInfo err = json.NewDecoder(response.Body).Decode(&minfo) if err != nil { return nil, err @@ -71,7 +71,7 @@ func (self *HTTPContainerInfoGetter) GetMachineInfo(host string) (*info.MachineI return &minfo, nil } -func (self *HTTPContainerInfoGetter) getContainerInfo(host, path string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { +func (self *HTTPContainerInfoGetter) getContainerInfo(host, path string, req *cadvisorApi.ContainerInfoRequest) (*cadvisorApi.ContainerInfo, error) { var body io.Reader if req != nil { content, err := json.Marshal(req) @@ -102,7 +102,7 @@ func (self *HTTPContainerInfoGetter) getContainerInfo(host, path string, req *in return nil, fmt.Errorf("trying to get info for %v from %v; received status %v", path, host, response.Status) } - var cinfo info.ContainerInfo + var cinfo cadvisorApi.ContainerInfo err = json.NewDecoder(response.Body).Decode(&cinfo) if err != nil { return nil, err @@ -110,7 +110,7 @@ func (self *HTTPContainerInfoGetter) getContainerInfo(host, path string, req *in return &cinfo, nil } -func (self *HTTPContainerInfoGetter) GetContainerInfo(host, podID, containerID string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { +func (self *HTTPContainerInfoGetter) GetContainerInfo(host, podID, containerID string, req *cadvisorApi.ContainerInfoRequest) (*cadvisorApi.ContainerInfo, error) { return self.getContainerInfo( host, fmt.Sprintf("%v/%v", podID, containerID), @@ -118,6 +118,6 @@ func (self *HTTPContainerInfoGetter) GetContainerInfo(host, podID, containerID s ) } -func (self *HTTPContainerInfoGetter) GetRootInfo(host string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { +func (self *HTTPContainerInfoGetter) GetRootInfo(host string, req *cadvisorApi.ContainerInfoRequest) (*cadvisorApi.ContainerInfo, error) { return self.getContainerInfo(host, "", req) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/containerinfo_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/containerinfo_test.go index 43027f4b94be..2a3ffc64da55 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/containerinfo_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/containerinfo_test.go @@ -28,13 +28,13 @@ import ( "testing" "time" - "github.com/google/cadvisor/info" - itest "github.com/google/cadvisor/info/test" + cadvisorApi "github.com/google/cadvisor/info/v1" + cadvisorApiTest "github.com/google/cadvisor/info/v1/test" ) func testHTTPContainerInfoGetter( - req *info.ContainerInfoRequest, - cinfo *info.ContainerInfo, + req *cadvisorApi.ContainerInfoRequest, + cinfo *cadvisorApi.ContainerInfo, podID string, containerID string, status int, @@ -53,7 +53,7 @@ func testHTTPContainerInfoGetter( expectedPath, r.URL.Path) } - var receivedReq info.ContainerInfoRequest + var receivedReq cadvisorApi.ContainerInfoRequest err := json.NewDecoder(r.Body).Decode(&receivedReq) if err != nil { t.Fatal(err) @@ -62,7 +62,7 @@ func testHTTPContainerInfoGetter( // So changing req after Get*Info would be a race. expectedReq := req // Fill any empty fields with default value - if !reflect.DeepEqual(expectedReq, &receivedReq) { + if !expectedReq.Equals(receivedReq) { t.Errorf("received wrong request") } err = json.NewEncoder(w).Encode(cinfo) @@ -87,7 +87,7 @@ func testHTTPContainerInfoGetter( Port: port, } - var receivedContainerInfo *info.ContainerInfo + var receivedContainerInfo *cadvisorApi.ContainerInfo if len(podID) > 0 && len(containerID) > 0 { receivedContainerInfo, err = containerInfoGetter.GetContainerInfo(parts[0], podID, containerID, req) } else { @@ -109,10 +109,10 @@ func testHTTPContainerInfoGetter( } func TestHTTPContainerInfoGetterGetContainerInfoSuccessfully(t *testing.T) { - req := &info.ContainerInfoRequest{ + req := &cadvisorApi.ContainerInfoRequest{ NumStats: 10, } - cinfo := itest.GenerateRandomContainerInfo( + cinfo := cadvisorApiTest.GenerateRandomContainerInfo( "dockerIDWhichWillNotBeChecked", // docker ID 2, // Number of cores req, @@ -122,10 +122,10 @@ func TestHTTPContainerInfoGetterGetContainerInfoSuccessfully(t *testing.T) { } func TestHTTPContainerInfoGetterGetRootInfoSuccessfully(t *testing.T) { - req := &info.ContainerInfoRequest{ + req := &cadvisorApi.ContainerInfoRequest{ NumStats: 10, } - cinfo := itest.GenerateRandomContainerInfo( + cinfo := cadvisorApiTest.GenerateRandomContainerInfo( "dockerIDWhichWillNotBeChecked", // docker ID 2, // Number of cores req, @@ -135,10 +135,10 @@ func TestHTTPContainerInfoGetterGetRootInfoSuccessfully(t *testing.T) { } func TestHTTPContainerInfoGetterGetContainerInfoWithError(t *testing.T) { - req := &info.ContainerInfoRequest{ + req := &cadvisorApi.ContainerInfoRequest{ NumStats: 10, } - cinfo := itest.GenerateRandomContainerInfo( + cinfo := cadvisorApiTest.GenerateRandomContainerInfo( "dockerIDWhichWillNotBeChecked", // docker ID 2, // Number of cores req, @@ -148,10 +148,10 @@ func TestHTTPContainerInfoGetterGetContainerInfoWithError(t *testing.T) { } func TestHTTPContainerInfoGetterGetRootInfoWithError(t *testing.T) { - req := &info.ContainerInfoRequest{ + req := &cadvisorApi.ContainerInfoRequest{ NumStats: 10, } - cinfo := itest.GenerateRandomContainerInfo( + cinfo := cadvisorApiTest.GenerateRandomContainerInfo( "dockerIDWhichWillNotBeChecked", // docker ID 2, // Number of cores req, @@ -161,7 +161,7 @@ func TestHTTPContainerInfoGetterGetRootInfoWithError(t *testing.T) { } func TestHTTPGetMachineInfo(t *testing.T) { - mspec := &info.MachineInfo{ + mspec := &cadvisorApi.MachineInfo{ NumCores: 4, MemoryCapacity: 2048, } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/endpoints.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/endpoints.go index 9a83389971d7..7a2406bf1881 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/endpoints.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/endpoints.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -36,7 +37,7 @@ type EndpointsInterface interface { List(selector labels.Selector) (*api.EndpointsList, error) Get(name string) (*api.Endpoints, error) Update(endpoints *api.Endpoints) (*api.Endpoints, error) - Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } // endpoints implements EndpointsInterface @@ -60,7 +61,12 @@ func (c *endpoints) Create(endpoints *api.Endpoints) (*api.Endpoints, error) { // List takes a selector, and returns the list of endpoints that match that selector func (c *endpoints) List(selector labels.Selector) (result *api.EndpointsList, err error) { result = &api.EndpointsList{} - err = c.r.Get().Namespace(c.ns).Resource("endpoints").SelectorParam("labels", selector).Do().Into(result) + err = c.r.Get(). + Namespace(c.ns). + Resource("endpoints"). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), selector). + Do(). + Into(result) return } @@ -76,14 +82,14 @@ func (c *endpoints) Get(name string) (result *api.Endpoints, err error) { } // Watch returns a watch.Interface that watches the requested endpoints for a service. -func (c *endpoints) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *endpoints) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { return c.r.Get(). Prefix("watch"). Namespace(c.ns). Resource("endpoints"). Param("resourceVersion", resourceVersion). - SelectorParam("labels", label). - SelectorParam("fields", field). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.r.APIVersion()), field). Watch() } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/events.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/events.go index 04c7a5ea03ca..6b3e8380d5f1 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/events.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/events.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" @@ -35,11 +36,12 @@ type EventNamespacer interface { type EventInterface interface { Create(event *api.Event) (*api.Event, error) Update(event *api.Event) (*api.Event, error) - List(label, field labels.Selector) (*api.EventList, error) + List(label labels.Selector, field fields.Selector) (*api.EventList, error) Get(name string) (*api.Event, error) - Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) // Search finds events about the specified object Search(objOrRef runtime.Object) (*api.EventList, error) + Delete(name string) error } // events implements Events interface @@ -95,13 +97,13 @@ func (e *events) Update(event *api.Event) (*api.Event, error) { } // List returns a list of events matching the selectors. -func (e *events) List(label, field labels.Selector) (*api.EventList, error) { +func (e *events) List(label labels.Selector, field fields.Selector) (*api.EventList, error) { result := &api.EventList{} err := e.client.Get(). NamespaceIfScoped(e.namespace, len(e.namespace) > 0). Resource("events"). - SelectorParam("labels", label). - SelectorParam("fields", field). + LabelsSelectorParam(api.LabelSelectorQueryParam(e.client.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(e.client.APIVersion()), field). Do(). Into(result) return result, err @@ -124,14 +126,14 @@ func (e *events) Get(name string) (*api.Event, error) { } // Watch starts watching for events matching the given selectors. -func (e *events) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (e *events) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { return e.client.Get(). Prefix("watch"). NamespaceIfScoped(e.namespace, len(e.namespace) > 0). Resource("events"). Param("resourceVersion", resourceVersion). - SelectorParam("labels", label). - SelectorParam("fields", field). + LabelsSelectorParam(api.LabelSelectorQueryParam(e.client.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(e.client.APIVersion()), field). Watch() } @@ -146,7 +148,7 @@ func (e *events) Search(objOrRef runtime.Object) (*api.EventList, error) { if e.namespace != "" && ref.Namespace != e.namespace { return nil, fmt.Errorf("won't be able to find any events of namespace '%v' in namespace '%v'", ref.Namespace, e.namespace) } - fields := labels.Set{} + fields := fields.Set{} if ref.Kind != "" { fields["involvedObject.kind"] = ref.Kind } @@ -161,3 +163,13 @@ func (e *events) Search(objOrRef runtime.Object) (*api.EventList, error) { } return e.List(labels.Everything(), fields.AsSelector()) } + +// Delete deletes an existing event. +func (e *events) Delete(name string) error { + return e.client.Delete(). + NamespaceIfScoped(e.namespace, len(e.namespace) > 0). + Resource("events"). + Name(name). + Do(). + Error() +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/events_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/events_test.go index 41920a226563..ed07998930d8 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/events_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/events_test.go @@ -23,6 +23,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -159,7 +160,7 @@ func TestEventList(t *testing.T) { Response: Response{StatusCode: 200, Body: eventList}, } response, err := c.Setup().Events(ns).List(labels.Everything(), - labels.Everything()) + fields.Everything()) if err != nil { t.Errorf("%#v should be nil.", err) @@ -175,3 +176,13 @@ func TestEventList(t *testing.T) { t.Errorf("%#v != %#v.", e, r) } } + +func TestEventDelete(t *testing.T) { + ns := api.NamespaceDefault + c := &testClient{ + Request: testRequest{Method: "DELETE", Path: "/events/foo"}, + Response: Response{StatusCode: 200}, + } + err := c.Setup().Events(ns).Delete("foo") + c.Validate(t, nil, err) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake.go index e00ffb17c0b1..e0e496d9b942 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake.go @@ -21,6 +21,7 @@ import ( "net/url" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" @@ -34,21 +35,22 @@ type FakeAction struct { // Fake implements Interface. Meant to be embedded into a struct to get a default // implementation. This makes faking out just the method you want to test easier. type Fake struct { - Actions []FakeAction - PodsList api.PodList - CtrlList api.ReplicationControllerList - Ctrl api.ReplicationController - ServiceList api.ServiceList - EndpointsList api.EndpointsList - MinionsList api.NodeList - EventsList api.EventList - LimitRangesList api.LimitRangeList - ResourceQuotasList api.ResourceQuotaList - NamespacesList api.NamespaceList - SecretList api.SecretList - Secret api.Secret - Err error - Watch watch.Interface + Actions []FakeAction + PodsList api.PodList + CtrlList api.ReplicationControllerList + Ctrl api.ReplicationController + ServiceList api.ServiceList + EndpointsList api.EndpointsList + MinionsList api.NodeList + EventsList api.EventList + LimitRangesList api.LimitRangeList + ResourceQuotaStatus api.ResourceQuota + ResourceQuotasList api.ResourceQuotaList + NamespacesList api.NamespaceList + SecretList api.SecretList + Secret api.Secret + Err error + Watch watch.Interface } func (c *Fake) LimitRanges(namespace string) LimitRangeInterface { @@ -59,10 +61,6 @@ func (c *Fake) ResourceQuotas(namespace string) ResourceQuotaInterface { return &FakeResourceQuotas{Fake: c, Namespace: namespace} } -func (c *Fake) ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterface { - return &FakeResourceQuotaUsages{Fake: c, Namespace: namespace} -} - func (c *Fake) ReplicationControllers(namespace string) ReplicationControllerInterface { return &FakeReplicationControllers{Fake: c, Namespace: namespace} } @@ -123,19 +121,19 @@ type FakeRESTClient struct { } func (c *FakeRESTClient) Get() *Request { - return NewRequest(c, "GET", &url.URL{Host: "localhost"}, c.Codec, c.Legacy, c.Legacy) + return NewRequest(c, "GET", &url.URL{Host: "localhost"}, testapi.Version(), c.Codec, c.Legacy, c.Legacy) } func (c *FakeRESTClient) Put() *Request { - return NewRequest(c, "PUT", &url.URL{Host: "localhost"}, c.Codec, c.Legacy, c.Legacy) + return NewRequest(c, "PUT", &url.URL{Host: "localhost"}, testapi.Version(), c.Codec, c.Legacy, c.Legacy) } func (c *FakeRESTClient) Post() *Request { - return NewRequest(c, "POST", &url.URL{Host: "localhost"}, c.Codec, c.Legacy, c.Legacy) + return NewRequest(c, "POST", &url.URL{Host: "localhost"}, testapi.Version(), c.Codec, c.Legacy, c.Legacy) } func (c *FakeRESTClient) Delete() *Request { - return NewRequest(c, "DELETE", &url.URL{Host: "localhost"}, c.Codec, c.Legacy, c.Legacy) + return NewRequest(c, "DELETE", &url.URL{Host: "localhost"}, testapi.Version(), c.Codec, c.Legacy, c.Legacy) } func (c *FakeRESTClient) Do(req *http.Request) (*http.Response, error) { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_endpoints.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_endpoints.go index 74c492e5a4e7..6d88cc4e06f9 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_endpoints.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_endpoints.go @@ -18,6 +18,7 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -44,7 +45,7 @@ func (c *FakeEndpoints) Get(name string) (*api.Endpoints, error) { return &api.Endpoints{}, nil } -func (c *FakeEndpoints) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *FakeEndpoints) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-endpoints", Value: resourceVersion}) return c.Fake.Watch, c.Fake.Err } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_events.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_events.go index d796e910f083..aee04e135219 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_events.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_events.go @@ -18,6 +18,7 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" @@ -42,7 +43,7 @@ func (c *FakeEvents) Update(event *api.Event) (*api.Event, error) { } // List returns a list of events matching the selectors. -func (c *FakeEvents) List(label, field labels.Selector) (*api.EventList, error) { +func (c *FakeEvents) List(label labels.Selector, field fields.Selector) (*api.EventList, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-events"}) return &c.Fake.EventsList, nil } @@ -54,7 +55,7 @@ func (c *FakeEvents) Get(id string) (*api.Event, error) { } // Watch starts watching for events matching the given selectors. -func (c *FakeEvents) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *FakeEvents) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-events", Value: resourceVersion}) return c.Fake.Watch, c.Fake.Err } @@ -64,3 +65,8 @@ func (c *FakeEvents) Search(objOrRef runtime.Object) (*api.EventList, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "search-events"}) return &c.Fake.EventsList, nil } + +func (c *FakeEvents) Delete(name string) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-event", Value: name}) + return nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_limit_ranges.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_limit_ranges.go index 9d1c7c237c57..a74e671240c8 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_limit_ranges.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_limit_ranges.go @@ -18,7 +18,9 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) // FakeLimitRanges implements PodsInterface. Meant to be embedded into a struct to get a default @@ -52,3 +54,8 @@ func (c *FakeLimitRanges) Update(limitRange *api.LimitRange) (*api.LimitRange, e c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-limitRange", Value: limitRange.Name}) return &api.LimitRange{}, nil } + +func (c *FakeLimitRanges) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-limitRange", Value: resourceVersion}) + return c.Fake.Watch, nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_namespaces.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_namespaces.go index 09c182288371..197b56767f1a 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_namespaces.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_namespaces.go @@ -18,6 +18,7 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -53,7 +54,7 @@ func (c *FakeNamespaces) Update(namespace *api.Namespace) (*api.Namespace, error return &api.Namespace{}, nil } -func (c *FakeNamespaces) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *FakeNamespaces) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-namespaces", Value: resourceVersion}) return c.Fake.Watch, nil } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_pods.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_pods.go index 27e9e7b55512..970904cca272 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_pods.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_pods.go @@ -18,7 +18,9 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) // FakePods implements PodsInterface. Meant to be embedded into a struct to get a default @@ -52,3 +54,18 @@ func (c *FakePods) Update(pod *api.Pod) (*api.Pod, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-pod", Value: pod.Name}) return &api.Pod{}, nil } + +func (c *FakePods) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-pods", Value: resourceVersion}) + return c.Fake.Watch, c.Fake.Err +} + +func (c *FakePods) Bind(bind *api.Binding) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "bind-pod", Value: bind.Name}) + return nil +} + +func (c *FakePods) UpdateStatus(name string, status *api.PodStatus) (*api.Pod, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-status-pod", Value: name}) + return &api.Pod{}, nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_replication_controllers.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_replication_controllers.go index 17a1f02f9df6..6e4f9f1e4434 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_replication_controllers.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_replication_controllers.go @@ -18,6 +18,7 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -54,7 +55,7 @@ func (c *FakeReplicationControllers) Delete(controller string) error { return nil } -func (c *FakeReplicationControllers) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *FakeReplicationControllers) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-controllers", Value: resourceVersion}) return c.Fake.Watch, nil } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_resource_quotas.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_resource_quotas.go index f7b870ed127b..5024ec475600 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_resource_quotas.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_resource_quotas.go @@ -18,7 +18,9 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) // FakeResourceQuotas implements ResourceQuotaInterface. Meant to be embedded into a struct to get a default @@ -52,3 +54,14 @@ func (c *FakeResourceQuotas) Update(resourceQuota *api.ResourceQuota) (*api.Reso c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-resourceQuota", Value: resourceQuota.Name}) return &api.ResourceQuota{}, nil } + +func (c *FakeResourceQuotas) Status(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-status-resourceQuota", Value: resourceQuota.Name}) + c.Fake.ResourceQuotaStatus = *resourceQuota + return &api.ResourceQuota{}, nil +} + +func (c *FakeResourceQuotas) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-resourceQuota", Value: resourceVersion}) + return c.Fake.Watch, nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_secrets.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_secrets.go index 284fff567faf..42b581e3de55 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_secrets.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_secrets.go @@ -18,6 +18,7 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -29,7 +30,7 @@ type FakeSecrets struct { Namespace string } -func (c *FakeSecrets) List(labels, fields labels.Selector) (*api.SecretList, error) { +func (c *FakeSecrets) List(labels labels.Selector, field fields.Selector) (*api.SecretList, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-secrets"}) return &c.Fake.SecretList, c.Fake.Err } @@ -54,7 +55,7 @@ func (c *FakeSecrets) Delete(secret string) error { return nil } -func (c *FakeSecrets) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *FakeSecrets) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-secrets", Value: resourceVersion}) return c.Fake.Watch, c.Fake.Err } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_services.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_services.go index 5724a94dcce6..7499f81420f2 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_services.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/fake_services.go @@ -18,6 +18,7 @@ package client import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -54,7 +55,7 @@ func (c *FakeServices) Delete(service string) error { return nil } -func (c *FakeServices) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *FakeServices) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-services", Value: resourceVersion}) return c.Fake.Watch, c.Fake.Err } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/kubelet.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/kubelet.go index 72882b5af7a6..16f433cf2236 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/kubelet.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/kubelet.go @@ -22,12 +22,14 @@ import ( "io/ioutil" "net" "net/http" + "net/url" "strconv" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" httprobe "github.com/GoogleCloudPlatform/kubernetes/pkg/probe/http" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) // ErrPodInfoNotAvailable may be returned when the requested pod info is not available. @@ -37,6 +39,7 @@ var ErrPodInfoNotAvailable = errors.New("no pod info available") type KubeletClient interface { KubeletHealthChecker PodInfoGetter + NodeInfoGetter } // KubeletHealthchecker is an interface for healthchecking kubelets @@ -52,6 +55,10 @@ type PodInfoGetter interface { GetPodStatus(host, podNamespace, podID string) (api.PodStatusResult, error) } +type NodeInfoGetter interface { + GetNodeInfo(host string) (api.NodeInfo, error) +} + // HTTPKubeletClient is the default implementation of PodInfoGetter and KubeletHealthchecker, accesses the kubelet over HTTP. type HTTPKubeletClient struct { Client *http.Client @@ -85,57 +92,62 @@ func NewKubeletClient(config *KubeletConfig) (KubeletClient, error) { }, nil } -func (c *HTTPKubeletClient) url(host string) string { - scheme := "http://" +func (c *HTTPKubeletClient) url(host, path, query string) string { + scheme := "http" if c.EnableHttps { - scheme = "https://" + scheme = "https" } - return fmt.Sprintf( - "%s%s", - scheme, - net.JoinHostPort(host, strconv.FormatUint(uint64(c.Port), 10))) + return (&url.URL{ + Scheme: scheme, + Host: net.JoinHostPort(host, strconv.FormatUint(uint64(c.Port), 10)), + Path: path, + RawQuery: query, + }).String() } // GetPodInfo gets information about the specified pod. func (c *HTTPKubeletClient) GetPodStatus(host, podNamespace, podID string) (api.PodStatusResult, error) { - request, err := http.NewRequest( - "GET", - fmt.Sprintf( - "%s/api/v1beta1/podInfo?podID=%s&podNamespace=%s", - c.url(host), - podID, - podNamespace), - nil) status := api.PodStatusResult{} + query := url.Values{"podID": {podID}, "podNamespace": {podNamespace}} + response, err := c.getEntity(host, "/api/v1beta1/podInfo", query.Encode(), &status) + if response != nil && response.StatusCode == http.StatusNotFound { + return status, ErrPodInfoNotAvailable + } + return status, err +} + +// GetNodeInfo gets information about the specified node. +func (c *HTTPKubeletClient) GetNodeInfo(host string) (api.NodeInfo, error) { + info := api.NodeInfo{} + _, err := c.getEntity(host, "/api/v1beta1/nodeInfo", "", &info) + return info, err +} + +// getEntity might return a nil response. +func (c *HTTPKubeletClient) getEntity(host, path, query string, entity runtime.Object) (*http.Response, error) { + request, err := http.NewRequest("GET", c.url(host, path, query), nil) if err != nil { - return status, err + return nil, err } response, err := c.Client.Do(request) if err != nil { - return status, err + return response, err } defer response.Body.Close() - if response.StatusCode == http.StatusNotFound { - return status, ErrPodInfoNotAvailable - } if response.StatusCode >= 300 || response.StatusCode < 200 { - return status, fmt.Errorf("kubelet %q server responded with HTTP error code %d for pod %s/%s", host, response.StatusCode, podNamespace, podID) + return response, fmt.Errorf("kubelet %q server responded with HTTP error code %d", host, response.StatusCode) } body, err := ioutil.ReadAll(response.Body) if err != nil { - return status, err + return response, err } - // Check that this data can be unmarshalled - err = latest.Codec.DecodeInto(body, &status) - if err != nil { - return status, err - } - return status, nil + err = latest.Codec.DecodeInto(body, entity) + return response, err } func (c *HTTPKubeletClient) HealthCheck(host string) (probe.Result, error) { - return httprobe.DoHTTPProbe(fmt.Sprintf("%s/healthz", c.url(host)), c.Client) + return httprobe.DoHTTPProbe(c.url(host, "/healthz", ""), c.Client) } // FakeKubeletClient is a fake implementation of KubeletClient which returns an error @@ -148,6 +160,11 @@ func (c FakeKubeletClient) GetPodStatus(host, podNamespace string, podID string) return api.PodStatusResult{}, errors.New("Not Implemented") } +// GetNodeInfo is a fake implementation of PodInfoGetter.GetNodeInfo +func (c FakeKubeletClient) GetNodeInfo(host string) (api.NodeInfo, error) { + return api.NodeInfo{}, errors.New("Not Implemented") +} + func (c FakeKubeletClient) HealthCheck(host string) (probe.Result, error) { return probe.Unknown, errors.New("Not Implemented") } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/limit_ranges.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/limit_ranges.go index fa03c61178e2..d234643a1b9a 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/limit_ranges.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/limit_ranges.go @@ -21,7 +21,9 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) // LimitRangesNamespacer has methods to work with LimitRange resources in a namespace @@ -36,6 +38,7 @@ type LimitRangeInterface interface { Delete(name string) error Create(limitRange *api.LimitRange) (*api.LimitRange, error) Update(limitRange *api.LimitRange) (*api.LimitRange, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } // limitRanges implements LimitRangesNamespacer interface @@ -55,7 +58,7 @@ func newLimitRanges(c *Client, namespace string) *limitRanges { // List takes a selector, and returns the list of limitRanges that match that selector. func (c *limitRanges) List(selector labels.Selector) (result *api.LimitRangeList, err error) { result = &api.LimitRangeList{} - err = c.r.Get().Namespace(c.ns).Resource("limitRanges").SelectorParam("labels", selector).Do().Into(result) + err = c.r.Get().Namespace(c.ns).Resource("limitRanges").LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), selector).Do().Into(result) return } @@ -92,3 +95,15 @@ func (c *limitRanges) Update(limitRange *api.LimitRange) (result *api.LimitRange err = c.r.Put().Namespace(c.ns).Resource("limitRanges").Name(limitRange.Name).Body(limitRange).Do().Into(result) return } + +// Watch returns a watch.Interface that watches the requested resource +func (c *limitRanges) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return c.r.Get(). + Prefix("watch"). + Namespace(c.ns). + Resource("limitRanges"). + Param("resourceVersion", resourceVersion). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.r.APIVersion()), field). + Watch() +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/limit_ranges_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/limit_ranges_test.go index 126baa7ac5f6..802bff09edd6 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/limit_ranges_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/limit_ranges_test.go @@ -17,12 +17,13 @@ limitations under the License. package client import ( + "net/url" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - //"github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) func TestLimitRangeCreate(t *testing.T) { @@ -192,3 +193,12 @@ func TestLimitRangeDelete(t *testing.T) { err := c.Setup().LimitRanges(ns).Delete("foo") c.Validate(t, nil, err) } + +func TestLimitRangeWatch(t *testing.T) { + c := &testClient{ + Request: testRequest{Method: "GET", Path: "/watch/limitRanges", Query: url.Values{"resourceVersion": []string{}}}, + Response: Response{StatusCode: 200}, + } + _, err := c.Setup().LimitRanges(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), "") + c.Validate(t, nil, err) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/minions.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/minions.go index fb4c20cd9989..760296761b0c 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/minions.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/minions.go @@ -48,7 +48,7 @@ func newNodes(c *Client) *nodes { // resourceName returns node's URL resource name based on resource version. func (c *nodes) resourceName() string { - if preV1Beta3(c.r.APIVersion()) { + if api.PreV1Beta3(c.r.APIVersion()) { return "minions" } return "nodes" diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/namespaces.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/namespaces.go index 4bbdc76ec07a..8285f80b7c06 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/namespaces.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/namespaces.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -35,7 +36,7 @@ type NamespaceInterface interface { List(selector labels.Selector) (*api.NamespaceList, error) Delete(name string) error Update(item *api.Namespace) (*api.Namespace, error) - Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } // namespaces implements NamespacesInterface @@ -58,7 +59,7 @@ func (c *namespaces) Create(namespace *api.Namespace) (*api.Namespace, error) { // List lists all the namespaces in the cluster. func (c *namespaces) List(selector labels.Selector) (*api.NamespaceList, error) { result := &api.NamespaceList{} - err := c.r.Get().Resource("namespaces").SelectorParam("labels", selector).Do().Into(result) + err := c.r.Get().Resource("namespaces").LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), selector).Do().Into(result) return result, err } @@ -90,12 +91,12 @@ func (c *namespaces) Delete(name string) error { } // Watch returns a watch.Interface that watches the requested namespaces. -func (c *namespaces) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *namespaces) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { return c.r.Get(). Prefix("watch"). Resource("namespaces"). Param("resourceVersion", resourceVersion). - SelectorParam("labels", label). - SelectorParam("fields", field). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.r.APIVersion()), field). Watch() } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/namespaces_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/namespaces_test.go index 6bc3dfe52914..7777c65581fc 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/namespaces_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/namespaces_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) @@ -139,6 +140,6 @@ func TestNamespaceWatch(t *testing.T) { Request: testRequest{Method: "GET", Path: "/watch/namespaces", Query: url.Values{"resourceVersion": []string{}}}, Response: Response{StatusCode: 200}, } - _, err := c.Setup().Namespaces().Watch(labels.Everything(), labels.Everything(), "") + _, err := c.Setup().Namespaces().Watch(labels.Everything(), fields.Everything(), "") c.Validate(t, nil, err) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/pods.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/pods.go index 7782b948a862..fc50cbc65f74 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/pods.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/pods.go @@ -21,7 +21,9 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) // PodsNamespacer has methods to work with Pod resources in a namespace @@ -36,6 +38,9 @@ type PodInterface interface { Delete(name string) error Create(pod *api.Pod) (*api.Pod, error) Update(pod *api.Pod) (*api.Pod, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + Bind(binding *api.Binding) error + UpdateStatus(name string, status *api.PodStatus) (*api.Pod, error) } // pods implements PodsNamespacer interface @@ -55,11 +60,11 @@ func newPods(c *Client, namespace string) *pods { // List takes a selector, and returns the list of pods that match that selector. func (c *pods) List(selector labels.Selector) (result *api.PodList, err error) { result = &api.PodList{} - err = c.r.Get().Namespace(c.ns).Resource("pods").SelectorParam("labels", selector).Do().Into(result) + err = c.r.Get().Namespace(c.ns).Resource("pods").LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), selector).Do().Into(result) return } -// GetPod takes the name of the pod, and returns the corresponding Pod object, and an error if it occurs +// Get takes the name of the pod, and returns the corresponding Pod object, and an error if it occurs func (c *pods) Get(name string) (result *api.Pod, err error) { if len(name) == 0 { return nil, errors.New("name is required parameter to Get") @@ -70,19 +75,19 @@ func (c *pods) Get(name string) (result *api.Pod, err error) { return } -// DeletePod takes the name of the pod, and returns an error if one occurs +// Delete takes the name of the pod, and returns an error if one occurs func (c *pods) Delete(name string) error { return c.r.Delete().Namespace(c.ns).Resource("pods").Name(name).Do().Error() } -// CreatePod takes the representation of a pod. Returns the server's representation of the pod, and an error, if it occurs. +// Create takes the representation of a pod. Returns the server's representation of the pod, and an error, if it occurs. func (c *pods) Create(pod *api.Pod) (result *api.Pod, err error) { result = &api.Pod{} err = c.r.Post().Namespace(c.ns).Resource("pods").Body(pod).Do().Into(result) return } -// UpdatePod takes the representation of a pod to update. Returns the server's representation of the pod, and an error, if it occurs. +// Update takes the representation of a pod to update. Returns the server's representation of the pod, and an error, if it occurs. func (c *pods) Update(pod *api.Pod) (result *api.Pod, err error) { result = &api.Pod{} if len(pod.ResourceVersion) == 0 { @@ -92,3 +97,32 @@ func (c *pods) Update(pod *api.Pod) (result *api.Pod, err error) { err = c.r.Put().Namespace(c.ns).Resource("pods").Name(pod.Name).Body(pod).Do().Into(result) return } + +// Watch returns a watch.Interface that watches the requested pods. +func (c *pods) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return c.r.Get(). + Prefix("watch"). + Namespace(c.ns). + Resource("pods"). + Param("resourceVersion", resourceVersion). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), label). + FieldsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), field). + Watch() +} + +// Bind applies the provided binding to the named pod in the current namespace (binding.Namespace is ignored). +func (c *pods) Bind(binding *api.Binding) error { + return c.r.Post().Namespace(c.ns).Resource("pods").Name(binding.Name).SubResource("binding").Body(binding).Do().Error() +} + +// UpdateStatus takes the name of the pod and the new status. Returns the server's representation of the pod, and an error, if it occurs. +func (c *pods) UpdateStatus(name string, newStatus *api.PodStatus) (result *api.Pod, err error) { + result = &api.Pod{} + pod, err := c.Get(name) + if err != nil { + return + } + pod.Status = *newStatus + err = c.r.Put().Namespace(c.ns).Resource("pods").Name(pod.Name).SubResource("status").Body(pod).Do().Into(result) + return +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/event.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/event.go index cf6755caf790..b41af7704e23 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/event.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/event.go @@ -35,20 +35,22 @@ const maxTriesPerEvent = 12 var sleepDuration = 10 * time.Second -// EventRecorder knows how to store events (client.Client implements it.) -// EventRecorder must respect the namespace that will be embedded in 'event'. -// It is assumed that EventRecorder will return the same sorts of errors as +// EventSink knows how to store events (client.Client implements it.) +// EventSink must respect the namespace that will be embedded in 'event'. +// It is assumed that EventSink will return the same sorts of errors as // pkg/client's REST client. -type EventRecorder interface { +type EventSink interface { Create(event *api.Event) (*api.Event, error) Update(event *api.Event) (*api.Event, error) } -// StartRecording starts sending events to recorder. Call once while initializing +var emptySource = api.EventSource{} + +// StartRecording starts sending events to a sink. Call once while initializing // your binary. Subsequent calls will be ignored. The return value can be ignored // or used to stop recording, if desired. // TODO: make me an object with parameterizable queue length and retry interval -func StartRecording(recorder EventRecorder, source api.EventSource) watch.Interface { +func StartRecording(sink EventSink) watch.Interface { // The default math/rand package functions aren't thread safe, so create a // new Rand object for each StartRecording call. randGen := rand.New(rand.NewSource(time.Now().UnixNano())) @@ -57,7 +59,6 @@ func StartRecording(recorder EventRecorder, source api.EventSource) watch.Interf // Events are safe to copy like this. eventCopy := *event event = &eventCopy - event.Source = source previousEvent := getEvent(event) updateExistingEvent := previousEvent.Count > 0 @@ -70,7 +71,7 @@ func StartRecording(recorder EventRecorder, source api.EventSource) watch.Interf tries := 0 for { - if recordEvent(recorder, event, updateExistingEvent) { + if recordEvent(sink, event, updateExistingEvent) { break } tries++ @@ -89,17 +90,17 @@ func StartRecording(recorder EventRecorder, source api.EventSource) watch.Interf }) } -// recordEvent attempts to write event to recorder. It returns true if the event +// recordEvent attempts to write event to a sink. It returns true if the event // was successfully recorded or discarded, false if it should be retried. // If updateExistingEvent is false, it creates a new event, otherwise it updates // existing event. -func recordEvent(recorder EventRecorder, event *api.Event, updateExistingEvent bool) bool { +func recordEvent(sink EventSink, event *api.Event, updateExistingEvent bool) bool { var newEvent *api.Event var err error if updateExistingEvent { - newEvent, err = recorder.Update(event) + newEvent, err = sink.Update(event) } else { - newEvent, err = recorder.Create(event) + newEvent, err = sink.Create(event) } if err == nil { addOrUpdateEvent(newEvent) @@ -165,24 +166,53 @@ const maxQueuedEvents = 1000 var events = watch.NewBroadcaster(maxQueuedEvents, watch.DropIfChannelFull) -// Event constructs an event from the given information and puts it in the queue for sending. -// 'object' is the object this event is about. Event will make a reference-- or you may also -// pass a reference to the object directly. -// 'reason' is the reason this event is generated. 'reason' should be short and unique; it will -// be used to automate handling of events, so imagine people writing switch statements to -// handle them. You want to make that easy. -// 'message' is intended to be human readable. -// -// The resulting event will be created in the same namespace as the reference object. -func Event(object runtime.Object, reason, message string) { +// EventRecorder knows how to record events for an EventSource. +type EventRecorder interface { + // Event constructs an event from the given information and puts it in the queue for sending. + // 'object' is the object this event is about. Event will make a reference-- or you may also + // pass a reference to the object directly. + // 'reason' is the reason this event is generated. 'reason' should be short and unique; it will + // be used to automate handling of events, so imagine people writing switch statements to + // handle them. You want to make that easy. + // 'message' is intended to be human readable. + // + // The resulting event will be created in the same namespace as the reference object. + Event(object runtime.Object, reason, message string) + + // Eventf is just like Event, but with Sprintf for the message field. + Eventf(object runtime.Object, reason, messageFmt string, args ...interface{}) +} + +// FromSource returns an EventRecorder that records events with the +// given event source. +func FromSource(source api.EventSource) EventRecorder { + return &recorderImpl{source} +} + +type recorderImpl struct { + source api.EventSource +} + +func (i *recorderImpl) Event(object runtime.Object, reason, message string) { ref, err := api.GetReference(object) if err != nil { glog.Errorf("Could not construct reference to: '%#v' due to: '%v'. Will not report event: '%v' '%v'", object, err, reason, message) return } - t := util.Now() - e := &api.Event{ + e := makeEvent(ref, reason, message) + e.Source = i.source + + events.Action(watch.Added, e) +} + +func (i *recorderImpl) Eventf(object runtime.Object, reason, messageFmt string, args ...interface{}) { + i.Event(object, reason, fmt.Sprintf(messageFmt, args...)) +} + +func makeEvent(ref *api.ObjectReference, reason, message string) *api.Event { + t := util.Now() + return &api.Event{ ObjectMeta: api.ObjectMeta{ Name: fmt.Sprintf("%v.%x", ref.Name, t.UnixNano()), Namespace: ref.Namespace, @@ -194,11 +224,4 @@ func Event(object runtime.Object, reason, message string) { LastTimestamp: t, Count: 1, } - - events.Action(watch.Added, e) -} - -// Eventf is just like Event, but with Sprintf for the message field. -func Eventf(object runtime.Object, reason, messageFmt string, args ...interface{}) { - Event(object, reason, fmt.Sprintf(messageFmt, args...)) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/event_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/event_test.go index 6337338e85c4..728c10301df6 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/event_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/event_test.go @@ -35,13 +35,13 @@ func init() { sleepDuration = 0 } -type testEventRecorder struct { +type testEventSink struct { OnCreate func(e *api.Event) (*api.Event, error) OnUpdate func(e *api.Event) (*api.Event, error) } // CreateEvent records the event for testing. -func (t *testEventRecorder) Create(e *api.Event) (*api.Event, error) { +func (t *testEventSink) Create(e *api.Event) (*api.Event, error) { if t.OnCreate != nil { return t.OnCreate(e) } @@ -49,7 +49,7 @@ func (t *testEventRecorder) Create(e *api.Event) (*api.Event, error) { } // UpdateEvent records the event for testing. -func (t *testEventRecorder) Update(e *api.Event) (*api.Event, error) { +func (t *testEventSink) Update(e *api.Event) (*api.Event, error) { if t.OnUpdate != nil { return t.OnUpdate(e) } @@ -273,7 +273,7 @@ func TestEventf(t *testing.T) { for _, item := range table { called := make(chan struct{}) - testEvents := testEventRecorder{ + testEvents := testEventSink{ OnCreate: func(event *api.Event) (*api.Event, error) { returnEvent, _ := validateEvent(event, item.expect, t) if item.expectUpdate { @@ -291,7 +291,7 @@ func TestEventf(t *testing.T) { return returnEvent, nil }, } - recorder := StartRecording(&testEvents, api.EventSource{Component: "eventTest"}) + recorder := StartRecording(&testEvents) logger := StartLogging(t.Logf) // Prove that it is useful logger2 := StartLogging(func(formatter string, args ...interface{}) { if e, a := item.expectLog, fmt.Sprintf(formatter, args...); e != a { @@ -300,7 +300,8 @@ func TestEventf(t *testing.T) { called <- struct{}{} }) - Eventf(item.obj, item.reason, item.messageFmt, item.elements...) + testSource := api.EventSource{Component: "eventTest"} + FromSource(testSource).Eventf(item.obj, item.reason, item.messageFmt, item.elements...) <-called <-called @@ -387,7 +388,7 @@ func TestWriteEventError(t *testing.T) { done := make(chan struct{}) defer StartRecording( - &testEventRecorder{ + &testEventSink{ OnCreate: func(event *api.Event) (*api.Event, error) { if event.Message == "finished" { close(done) @@ -405,13 +406,13 @@ func TestWriteEventError(t *testing.T) { return event, nil }, }, - api.EventSource{Component: "eventTest"}, ).Stop() + testSource := api.EventSource{Component: "eventTest"} for caseName := range table { - Event(ref, "Reason", caseName) + FromSource(testSource).Event(ref, "Reason", caseName) } - Event(ref, "Reason", "finished") + FromSource(testSource).Event(ref, "Reason", "finished") <-done for caseName, item := range table { @@ -427,7 +428,7 @@ func TestLotsOfEvents(t *testing.T) { // Fail each event a few times to ensure there's some load on the tested code. var counts [1000]int - testEvents := testEventRecorder{ + testEvents := testEventSink{ OnCreate: func(event *api.Event) (*api.Event, error) { num, err := strconv.Atoi(event.Message) if err != nil { @@ -442,7 +443,8 @@ func TestLotsOfEvents(t *testing.T) { return event, nil }, } - recorder := StartRecording(&testEvents, api.EventSource{Component: "eventTest"}) + recorder := StartRecording(&testEvents) + testSource := api.EventSource{Component: "eventTest"} logger := StartLogging(func(formatter string, args ...interface{}) { loggerCalled <- struct{}{} }) @@ -455,7 +457,7 @@ func TestLotsOfEvents(t *testing.T) { APIVersion: "v1beta1", } for i := 0; i < maxQueuedEvents; i++ { - go Event(ref, "Reason", strconv.Itoa(i)) + go FromSource(testSource).Event(ref, "Reason", strconv.Itoa(i)) } // Make sure no events were dropped by either of the listeners. for i := 0; i < maxQueuedEvents; i++ { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/events_cache_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/events_cache_test.go index 9fe5ccea7b4b..fb93745acdeb 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/events_cache_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/events_cache_test.go @@ -29,7 +29,7 @@ func TestAddOrUpdateEventNoExisting(t *testing.T) { Reason: "my reasons are many", Message: "my message is love", InvolvedObject: api.ObjectReference{ - Kind: "BoundPod", + Kind: "Pod", Name: "awesome.name", Namespace: "betterNamespace", UID: "C934D34AFB20242", @@ -143,7 +143,7 @@ func TestGetEventExisting(t *testing.T) { Reason: "do I exist", Message: "I do, oh my", InvolvedObject: api.ObjectReference{ - Kind: "BoundPod", + Kind: "Pod", Name: "clever.name.here", Namespace: "spaceOfName", UID: "D933D32AFB2A238", diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/fake.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/fake.go new file mode 100644 index 000000000000..1ba0f5abda8a --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/record/fake.go @@ -0,0 +1,28 @@ +/* +Copyright 2015 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 record + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// FakeRecorder is used as a fake during tests. +type FakeRecorder struct{} + +func (f *FakeRecorder) Event(object runtime.Object, reason, message string) {} + +func (f *FakeRecorder) Eventf(object runtime.Object, reason, messageFmt string, args ...interface{}) {} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/replication_controllers.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/replication_controllers.go index 10f902e6d9c6..cefbe61eaf4b 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/replication_controllers.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/replication_controllers.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -37,7 +38,7 @@ type ReplicationControllerInterface interface { Create(ctrl *api.ReplicationController) (*api.ReplicationController, error) Update(ctrl *api.ReplicationController) (*api.ReplicationController, error) Delete(name string) error - Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } // replicationControllers implements ReplicationControllersNamespacer interface @@ -54,7 +55,7 @@ func newReplicationControllers(c *Client, namespace string) *replicationControll // List takes a selector, and returns the list of replication controllers that match that selector. func (c *replicationControllers) List(selector labels.Selector) (result *api.ReplicationControllerList, err error) { result = &api.ReplicationControllerList{} - err = c.r.Get().Namespace(c.ns).Resource("replicationControllers").SelectorParam("labels", selector).Do().Into(result) + err = c.r.Get().Namespace(c.ns).Resource("replicationControllers").LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), selector).Do().Into(result) return } @@ -93,13 +94,13 @@ func (c *replicationControllers) Delete(name string) error { } // Watch returns a watch.Interface that watches the requested controllers. -func (c *replicationControllers) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *replicationControllers) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { return c.r.Get(). Prefix("watch"). Namespace(c.ns). Resource("replicationControllers"). Param("resourceVersion", resourceVersion). - SelectorParam("labels", label). - SelectorParam("fields", field). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.r.APIVersion()), field). Watch() } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/request.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/request.go index b5773dd475a7..5b94e979e2f2 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/request.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/request.go @@ -31,6 +31,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -62,6 +63,12 @@ func (u *UnexpectedStatusError) Error() string { return fmt.Sprintf("request [%+v] failed (%d) %s: %s", u.Request, u.Response.StatusCode, u.Response.Status, u.Body) } +// IsUnexpectedStatusError determines if err is due to an unexpected status from the server. +func IsUnexpectedStatusError(err error) bool { + _, ok := err.(*UnexpectedStatusError) + return ok +} + // RequestConstructionError is returned when there's an error assembling a request. type RequestConstructionError struct { Err error @@ -82,7 +89,7 @@ type Request struct { baseURL *url.URL codec runtime.Codec - // If true, add "?namespace=" as a query parameter, if false put ns/ in path + // If true, add "?namespace=" as a query parameter, if false put namespaces/ in path // Query parameter is considered legacy behavior namespaceInQuery bool // If true, lowercase resource prior to inserting into a path, if false, leave it as is. Preserving @@ -99,16 +106,23 @@ type Request struct { namespaceSet bool resource string resourceName string + subresource string selector labels.Selector timeout time.Duration + apiVersion string + // output err error body io.Reader + + // The constructed request and the response + req *http.Request + resp *http.Response } // NewRequest creates a new request helper object for accessing runtime.Objects on a server. -func NewRequest(client HTTPClient, verb string, baseURL *url.URL, +func NewRequest(client HTTPClient, verb string, baseURL *url.URL, apiVersion string, codec runtime.Codec, namespaceInQuery bool, preserveResourceCase bool) *Request { return &Request{ client: client, @@ -156,6 +170,21 @@ func (r *Request) Resource(resource string) *Request { return r } +// SubResource sets a sub-resource path which can be multiple segments segment after the resource +// name but before the suffix. +func (r *Request) SubResource(subresources ...string) *Request { + if r.err != nil { + return r + } + subresource := path.Join(subresources...) + if len(r.subresource) != 0 { + r.err = fmt.Errorf("subresource already set to %q, cannot change to %q", r.resource, subresource) + return r + } + r.subresource = subresource + return r +} + // Name sets the name of a resource to access (/[ns//]) func (r *Request) Name(resourceName string) *Request { if r.err != nil { @@ -229,23 +258,49 @@ func (r *Request) RequestURI(uri string) *Request { return r } -// ParseSelectorParam parses the given string as a resource label selector. +// ParseSelectorParam parses the given string as a resource selector. // This is a convenience function so you don't have to first check that it's a // validly formatted selector. func (r *Request) ParseSelectorParam(paramName, item string) *Request { if r.err != nil { return r } - sel, err := labels.ParseSelector(item) + var selector string + var err error + switch paramName { + case "labels": + var lsel labels.Selector + if lsel, err = labels.Parse(item); err == nil { + selector = lsel.String() + } + case "fields": + var fsel fields.Selector + if fsel, err = fields.ParseSelector(item); err == nil { + selector = fsel.String() + } + default: + err = fmt.Errorf("unknown parameter name '%s'", paramName) + } if err != nil { r.err = err return r } - return r.setParam(paramName, sel.String()) + return r.setParam(paramName, selector) } -// SelectorParam adds the given selector as a query parameter with the name paramName. -func (r *Request) SelectorParam(paramName string, s labels.Selector) *Request { +// FieldsSelectorParam adds the given selector as a query parameter with the name paramName. +func (r *Request) FieldsSelectorParam(paramName string, s fields.Selector) *Request { + if r.err != nil { + return r + } + if s.Empty() { + return r + } + return r.setParam(paramName, s.String()) +} + +// LabelsSelectorParam adds the given selector as a query parameter +func (r *Request) LabelsSelectorParam(paramName string, s labels.Selector) *Request { if r.err != nil { return r } @@ -341,8 +396,8 @@ func (r *Request) finalURL() string { p = path.Join(p, resource) } // Join trims trailing slashes, so preserve r.path's trailing slash for backwards compat if nothing was changed - if len(r.resourceName) != 0 || len(r.subpath) != 0 { - p = path.Join(p, r.resourceName, r.subpath) + if len(r.resourceName) != 0 || len(r.subpath) != 0 || len(r.subresource) != 0 { + p = path.Join(p, r.resourceName, r.subresource, r.subpath) } finalURL := *r.baseURL @@ -473,16 +528,8 @@ func (r *Request) Upgrade(config *Config, newRoundTripperFunc func(*tls.Config) return upgradeRoundTripper.NewConnection(resp) } -// Do formats and executes the request. Returns a Result object for easy response -// processing. -// -// Error type: -// * If the request can't be constructed, or an error happened earlier while building its -// arguments: *RequestConstructionError -// * If the server responds with a status: *errors.StatusError or *errors.UnexpectedObjectError -// * If the status code and body don't make sense together: *UnexpectedStatusError -// * http.Client.Do errors are returned directly. -func (r *Request) Do() Result { +// DoRaw executes a raw request which is not subject to interpretation as an API response. +func (r *Request) DoRaw() ([]byte, error) { client := r.client if client == nil { client = http.DefaultClient @@ -494,34 +541,33 @@ func (r *Request) Do() Result { for { if r.err != nil { - return Result{err: &RequestConstructionError{r.err}} + return nil, r.err } // TODO: added to catch programmer errors (invoking operations with an object with an empty namespace) if (r.verb == "GET" || r.verb == "PUT" || r.verb == "DELETE") && r.namespaceSet && len(r.resourceName) > 0 && len(r.namespace) == 0 { - return Result{err: &RequestConstructionError{fmt.Errorf("an empty namespace may not be set when a resource name is provided")}} + return nil, fmt.Errorf("an empty namespace may not be set when a resource name is provided") } if (r.verb == "POST") && r.namespaceSet && len(r.namespace) == 0 { - return Result{err: &RequestConstructionError{fmt.Errorf("an empty namespace may not be set during creation")}} + return nil, fmt.Errorf("an empty namespace may not be set during creation") } - req, err := http.NewRequest(r.verb, r.finalURL(), r.body) + var err error + r.req, err = http.NewRequest(r.verb, r.finalURL(), r.body) if err != nil { - return Result{err: &RequestConstructionError{err}} + return nil, err } - - resp, err := client.Do(req) + r.resp, err = client.Do(r.req) if err != nil { - return Result{err: err} + return nil, err } - - respBody, created, err := r.transformResponse(resp, req) + defer r.resp.Body.Close() // Check to see if we got a 429 Too Many Requests response code. - if resp.StatusCode == errors.StatusTooManyRequests { + if r.resp.StatusCode == errors.StatusTooManyRequests { if retries < 10 { retries++ - if waitFor := resp.Header.Get("Retry-After"); waitFor != "" { + if waitFor := r.resp.Header.Get("Retry-After"); waitFor != "" { delay, err := strconv.Atoi(waitFor) if err == nil { glog.V(4).Infof("Got a Retry-After %s response for attempt %d to %v", waitFor, retries, r.finalURL()) @@ -531,19 +577,34 @@ func (r *Request) Do() Result { } } } - return Result{respBody, created, err, r.codec} + body, err := ioutil.ReadAll(r.resp.Body) + if err != nil { + return nil, err + } + return body, err } } -// transformResponse converts an API response into a structured API object. -func (r *Request) transformResponse(resp *http.Response, req *http.Request) ([]byte, bool, error) { - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) +// Do formats and executes the request. Returns a Result object for easy response +// processing. +// +// Error type: +// * If the request can't be constructed, or an error happened earlier while building its +// arguments: *RequestConstructionError +// * If the server responds with a status: *errors.StatusError or *errors.UnexpectedObjectError +// * If the status code and body don't make sense together: *UnexpectedStatusError +// * http.Client.Do errors are returned directly. +func (r *Request) Do() Result { + body, err := r.DoRaw() if err != nil { - return nil, false, err + return Result{err: err} } + respBody, created, err := r.transformResponse(body, r.resp, r.req) + return Result{respBody, created, err, r.codec} +} +// transformResponse converts an API response into a structured API object. +func (r *Request) transformResponse(body []byte, resp *http.Response, req *http.Request) ([]byte, bool, error) { // Did the server give us a status response? isStatusResponse := false var status api.Status @@ -556,7 +617,8 @@ func (r *Request) transformResponse(resp *http.Response, req *http.Request) ([]b // no-op, we've been upgraded case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent: if !isStatusResponse { - var err error = &UnexpectedStatusError{ + var err error + err = &UnexpectedStatusError{ Request: req, Response: resp, Body: string(body), @@ -580,14 +642,15 @@ func (r *Request) transformResponse(resp *http.Response, req *http.Request) ([]b } // If the server gave us a status back, look at what it was. - if isStatusResponse && status.Status != api.StatusSuccess { + success := resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent + if isStatusResponse && (status.Status != api.StatusSuccess && !success) { // "Working" requests need to be handled specially. // "Failed" requests are clearly just an error and it makes sense to return them as such. return nil, false, errors.FromObject(&status) } created := resp.StatusCode == http.StatusCreated - return body, created, err + return body, created, nil } // Result contains the result of calling Request.Do(). diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/request_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/request_test.go index 942ccd7f5320..f0267026304f 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/request_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/request_test.go @@ -27,7 +27,7 @@ import ( "net/http/httptest" "net/url" "os" - // "reflect" + "reflect" "strings" "testing" "time" @@ -50,12 +50,11 @@ func TestRequestWithErrorWontChange(t *testing.T) { original := Request{err: errors.New("test")} r := original changed := r.Param("foo", "bar"). - SelectorParam("labels", labels.Set{"a": "b"}.AsSelector()). + LabelsSelectorParam(api.LabelSelectorQueryParam(testapi.Version()), labels.Set{"a": "b"}.AsSelector()). UintParam("uint", 1). AbsPath("/abs"). Prefix("test"). Suffix("testing"). - ParseSelectorParam("foo", "a=b"). Namespace("new"). Resource("foos"). Name("bars"). @@ -64,7 +63,7 @@ func TestRequestWithErrorWontChange(t *testing.T) { if changed != &r { t.Errorf("returned request should point to the same object") } - if !api.Semantic.DeepDerivative(changed, &original) { + if !reflect.DeepEqual(changed, &original) { t.Errorf("expected %#v, got %#v", &original, changed) } } @@ -129,6 +128,16 @@ func TestRequestOrdersNamespaceInPath(t *testing.T) { } } +func TestRequestOrdersSubResource(t *testing.T) { + r := (&Request{ + baseURL: &url.URL{}, + path: "/test/", + }).Name("bar").Resource("baz").Namespace("foo").Suffix("test").SubResource("a", "b") + if s := r.finalURL(); s != "/test/namespaces/foo/baz/bar/a/b/test" { + t.Errorf("namespace should be in order in path: %s", s) + } +} + func TestRequestSetTwiceError(t *testing.T) { if (&Request{}).Name("bar").Name("baz").err == nil { t.Errorf("setting name twice should result in error") @@ -139,12 +148,8 @@ func TestRequestSetTwiceError(t *testing.T) { if (&Request{}).Resource("bar").Resource("baz").err == nil { t.Errorf("setting resource twice should result in error") } -} - -func TestRequestParseSelectorParam(t *testing.T) { - r := (&Request{}).ParseSelectorParam("foo", "a") - if r.err == nil || r.params != nil { - t.Errorf("should have set err and left params nil: %#v", r) + if (&Request{}).SubResource("bar").SubResource("baz").err == nil { + t.Errorf("setting subresource twice should result in error") } } @@ -229,11 +234,15 @@ func TestTransformResponse(t *testing.T) { {Response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(invalid))}, Data: invalid}, } for i, test := range testCases { - r := NewRequest(nil, "", uri, testapi.Codec(), true, true) + r := NewRequest(nil, "", uri, testapi.Version(), testapi.Codec(), true, true) if test.Response.Body == nil { test.Response.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) } - response, created, err := r.transformResponse(test.Response, &http.Request{}) + body, err := ioutil.ReadAll(test.Response.Body) + if err != nil { + t.Errorf("failed to read body of response: %v", err) + } + response, created, err := r.transformResponse(body, test.Response, &http.Request{}) hasErr := err != nil if hasErr != test.Error { t.Errorf("%d: unexpected error: %t %v", i, test.Error, err) @@ -307,7 +316,11 @@ func TestTransformUnstructuredError(t *testing.T) { resourceName: testCase.Name, resource: testCase.Resource, } - _, _, err := r.transformResponse(testCase.Res, testCase.Req) + body, err := ioutil.ReadAll(testCase.Res.Body) + if err != nil { + t.Errorf("failed to read body: %v", err) + } + _, _, err = r.transformResponse(body, testCase.Res, testCase.Req) if !testCase.ErrFn(err) { t.Errorf("unexpected error: %v", err) continue @@ -519,7 +532,7 @@ func TestRequestUpgrade(t *testing.T) { Err: true, }, { - Request: NewRequest(nil, "", uri, testapi.Codec(), true, true), + Request: NewRequest(nil, "", uri, testapi.Version(), testapi.Codec(), true, true), Config: &Config{ Username: "u", Password: "p", @@ -528,7 +541,7 @@ func TestRequestUpgrade(t *testing.T) { Err: false, }, { - Request: NewRequest(nil, "", uri, testapi.Codec(), true, true), + Request: NewRequest(nil, "", uri, testapi.Version(), testapi.Codec(), true, true), Config: &Config{ BearerToken: "b", }, @@ -617,7 +630,6 @@ func TestDoRequestNewWay(t *testing.T) { obj, err := c.Verb("POST"). Prefix("foo", "bar"). Suffix("baz"). - ParseSelectorParam("labels", "name=foo"). Timeout(time.Second). Body([]byte(reqBody)). Do().Get() @@ -630,7 +642,7 @@ func TestDoRequestNewWay(t *testing.T) { } else if !api.Semantic.DeepDerivative(expectedObj, obj) { t.Errorf("Expected: %#v, got %#v", expectedObj, obj) } - fakeHandler.ValidateRequest(t, "/api/v1beta2/foo/bar/baz?labels=name%3Dfoo&timeout=1s", "POST", &reqBody) + fakeHandler.ValidateRequest(t, "/api/v1beta2/foo/bar/baz?timeout=1s", "POST", &reqBody) if fakeHandler.RequestReceived.Header["Authorization"] == nil { t.Errorf("Request is missing authorization header: %#v", *fakeHandler.RequestReceived) } @@ -652,7 +664,7 @@ func TestDoRequestNewWayReader(t *testing.T) { Resource("bar"). Name("baz"). Prefix("foo"). - SelectorParam("labels", labels.Set{"name": "foo"}.AsSelector()). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.APIVersion()), labels.Set{"name": "foo"}.AsSelector()). Timeout(time.Second). Body(bytes.NewBuffer(reqBodyExpected)). Do().Get() @@ -688,7 +700,7 @@ func TestDoRequestNewWayObj(t *testing.T) { Suffix("baz"). Name("bar"). Resource("foo"). - SelectorParam("labels", labels.Set{"name": "foo"}.AsSelector()). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.APIVersion()), labels.Set{"name": "foo"}.AsSelector()). Timeout(time.Second). Body(reqObj). Do().Get() @@ -737,7 +749,6 @@ func TestDoRequestNewWayFile(t *testing.T) { wasCreated := true obj, err := c.Verb("POST"). Prefix("foo/bar", "baz"). - ParseSelectorParam("labels", "name=foo"). Timeout(time.Second). Body(file.Name()). Do().WasCreated(&wasCreated).Get() @@ -754,7 +765,7 @@ func TestDoRequestNewWayFile(t *testing.T) { t.Errorf("expected object was not created") } tmpStr := string(reqBodyExpected) - fakeHandler.ValidateRequest(t, "/api/v1beta1/foo/bar/baz?labels=name%3Dfoo&timeout=1s", "POST", &tmpStr) + fakeHandler.ValidateRequest(t, "/api/v1beta1/foo/bar/baz?timeout=1s", "POST", &tmpStr) if fakeHandler.RequestReceived.Header["Authorization"] == nil { t.Errorf("Request is missing authorization header: %#v", *fakeHandler.RequestReceived) } @@ -779,7 +790,6 @@ func TestWasCreated(t *testing.T) { wasCreated := false obj, err := c.Verb("PUT"). Prefix("foo/bar", "baz"). - ParseSelectorParam("labels", "name=foo"). Timeout(time.Second). Body(reqBodyExpected). Do().WasCreated(&wasCreated).Get() @@ -797,7 +807,7 @@ func TestWasCreated(t *testing.T) { } tmpStr := string(reqBodyExpected) - fakeHandler.ValidateRequest(t, "/api/v1beta1/foo/bar/baz?labels=name%3Dfoo&timeout=1s", "PUT", &tmpStr) + fakeHandler.ValidateRequest(t, "/api/v1beta1/foo/bar/baz?timeout=1s", "PUT", &tmpStr) if fakeHandler.RequestReceived.Header["Authorization"] == nil { t.Errorf("Request is missing authorization header: %#v", *fakeHandler.RequestReceived) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quota_usages.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quota_usages.go deleted file mode 100644 index 1ba2c5e5e827..000000000000 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quota_usages.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -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 client - -import ( - "fmt" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -// ResourceQuotaUsagesNamespacer has methods to work with ResourceQuotaUsage resources in a namespace -type ResourceQuotaUsagesNamespacer interface { - ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterface -} - -// ResourceQuotaUsageInterface has methods to work with ResourceQuotaUsage resources. -type ResourceQuotaUsageInterface interface { - Create(resourceQuotaUsage *api.ResourceQuotaUsage) error -} - -// resourceQuotaUsages implements ResourceQuotaUsagesNamespacer interface -type resourceQuotaUsages struct { - r *Client - ns string -} - -// newResourceQuotaUsages returns a resourceQuotaUsages -func newResourceQuotaUsages(c *Client, namespace string) *resourceQuotaUsages { - return &resourceQuotaUsages{ - r: c, - ns: namespace, - } -} - -// Create takes the representation of a resourceQuotaUsage. Returns an error if the usage was not applied -func (c *resourceQuotaUsages) Create(resourceQuotaUsage *api.ResourceQuotaUsage) (err error) { - if len(resourceQuotaUsage.ResourceVersion) == 0 { - err = fmt.Errorf("invalid update object, missing resource version: %v", resourceQuotaUsage) - return - } - err = c.r.Post().Namespace(c.ns).Resource("resourceQuotaUsages").Body(resourceQuotaUsage).Do().Error() - return -} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quota_usages_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quota_usages_test.go deleted file mode 100644 index eb2c65f1bcb3..000000000000 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quota_usages_test.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -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 client - -import ( - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" -) - -func TestResourceQuotaUsageCreate(t *testing.T) { - ns := api.NamespaceDefault - resourceQuotaUsage := &api.ResourceQuotaUsage{ - ObjectMeta: api.ObjectMeta{ - Name: "abc", - Namespace: "foo", - ResourceVersion: "1", - }, - Status: api.ResourceQuotaStatus{ - Hard: api.ResourceList{ - api.ResourceCPU: resource.MustParse("100"), - api.ResourceMemory: resource.MustParse("10000"), - api.ResourcePods: resource.MustParse("10"), - api.ResourceServices: resource.MustParse("10"), - api.ResourceReplicationControllers: resource.MustParse("10"), - api.ResourceQuotas: resource.MustParse("10"), - }, - }, - } - c := &testClient{ - Request: testRequest{ - Method: "POST", - Path: buildResourcePath(ns, "/resourceQuotaUsages"), - Query: buildQueryValues(ns, nil), - Body: resourceQuotaUsage, - }, - Response: Response{StatusCode: 200, Body: resourceQuotaUsage}, - } - - err := c.Setup().ResourceQuotaUsages(ns).Create(resourceQuotaUsage) - if err != nil { - t.Errorf("Unexpected error %v", err) - } -} - -func TestInvalidResourceQuotaUsageCreate(t *testing.T) { - ns := api.NamespaceDefault - resourceQuotaUsage := &api.ResourceQuotaUsage{ - ObjectMeta: api.ObjectMeta{ - Name: "abc", - Namespace: "foo", - }, - Status: api.ResourceQuotaStatus{ - Hard: api.ResourceList{ - api.ResourceCPU: resource.MustParse("100"), - api.ResourceMemory: resource.MustParse("10000"), - api.ResourcePods: resource.MustParse("10"), - api.ResourceServices: resource.MustParse("10"), - api.ResourceReplicationControllers: resource.MustParse("10"), - api.ResourceQuotas: resource.MustParse("10"), - }, - }, - } - c := &testClient{ - Request: testRequest{ - Method: "POST", - Path: buildResourcePath(ns, "/resourceQuotaUsages"), - Query: buildQueryValues(ns, nil), - Body: resourceQuotaUsage, - }, - Response: Response{StatusCode: 200, Body: resourceQuotaUsage}, - } - - err := c.Setup().ResourceQuotaUsages(ns).Create(resourceQuotaUsage) - if err == nil { - t.Errorf("Expected error due to missing ResourceVersion") - } -} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quotas.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quotas.go index d0d51ea800a1..891989f2d826 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quotas.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quotas.go @@ -21,7 +21,9 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) // ResourceQuotasNamespacer has methods to work with ResourceQuota resources in a namespace @@ -36,6 +38,8 @@ type ResourceQuotaInterface interface { Delete(name string) error Create(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) Update(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) + Status(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } // resourceQuotas implements ResourceQuotasNamespacer interface @@ -55,7 +59,7 @@ func newResourceQuotas(c *Client, namespace string) *resourceQuotas { // List takes a selector, and returns the list of resourceQuotas that match that selector. func (c *resourceQuotas) List(selector labels.Selector) (result *api.ResourceQuotaList, err error) { result = &api.ResourceQuotaList{} - err = c.r.Get().Namespace(c.ns).Resource("resourceQuotas").SelectorParam("labels", selector).Do().Into(result) + err = c.r.Get().Namespace(c.ns).Resource("resourceQuotas").LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), selector).Do().Into(result) return } @@ -82,7 +86,7 @@ func (c *resourceQuotas) Create(resourceQuota *api.ResourceQuota) (result *api.R return } -// Update takes the representation of a resourceQuota to update. Returns the server's representation of the resourceQuota, and an error, if it occurs. +// Update takes the representation of a resourceQuota to update spec. Returns the server's representation of the resourceQuota, and an error, if it occurs. func (c *resourceQuotas) Update(resourceQuota *api.ResourceQuota) (result *api.ResourceQuota, err error) { result = &api.ResourceQuota{} if len(resourceQuota.ResourceVersion) == 0 { @@ -92,3 +96,26 @@ func (c *resourceQuotas) Update(resourceQuota *api.ResourceQuota) (result *api.R err = c.r.Put().Namespace(c.ns).Resource("resourceQuotas").Name(resourceQuota.Name).Body(resourceQuota).Do().Into(result) return } + +// Status takes the representation of a resourceQuota to update status. Returns the server's representation of the resourceQuota, and an error, if it occurs. +func (c *resourceQuotas) Status(resourceQuota *api.ResourceQuota) (result *api.ResourceQuota, err error) { + result = &api.ResourceQuota{} + if len(resourceQuota.ResourceVersion) == 0 { + err = fmt.Errorf("invalid update object, missing resource version: %v", resourceQuota) + return + } + err = c.r.Put().Namespace(c.ns).Resource("resourceQuotas").Name(resourceQuota.Name).SubResource("status").Body(resourceQuota).Do().Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested resource +func (c *resourceQuotas) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return c.r.Get(). + Prefix("watch"). + Namespace(c.ns). + Resource("resourceQuotas"). + Param("resourceVersion", resourceVersion). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.r.APIVersion()), field). + Watch() +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quotas_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quotas_test.go index 22eee5dadabf..1c9e71fee40b 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quotas_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/resource_quotas_test.go @@ -17,10 +17,12 @@ limitations under the License. package client import ( + "net/url" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) @@ -138,6 +140,33 @@ func TestResourceQuotaUpdate(t *testing.T) { c.Validate(t, response, err) } +func TestResourceQuotaStatusUpdate(t *testing.T) { + ns := api.NamespaceDefault + resourceQuota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + ResourceVersion: "1", + }, + Status: api.ResourceQuotaStatus{ + Hard: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + api.ResourcePods: resource.MustParse("10"), + api.ResourceServices: resource.MustParse("10"), + api.ResourceReplicationControllers: resource.MustParse("10"), + api.ResourceQuotas: resource.MustParse("10"), + }, + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: buildResourcePath(ns, "/resourceQuotas/abc/status"), Query: buildQueryValues(ns, nil)}, + Response: Response{StatusCode: 200, Body: resourceQuota}, + } + response, err := c.Setup().ResourceQuotas(ns).Status(resourceQuota) + c.Validate(t, response, err) +} + func TestInvalidResourceQuotaUpdate(t *testing.T) { ns := api.NamespaceDefault resourceQuota := &api.ResourceQuota{ @@ -175,3 +204,12 @@ func TestResourceQuotaDelete(t *testing.T) { err := c.Setup().ResourceQuotas(ns).Delete("foo") c.Validate(t, nil, err) } + +func TestResourceQuotaWatch(t *testing.T) { + c := &testClient{ + Request: testRequest{Method: "GET", Path: "/watch/resourceQuotas", Query: url.Values{"resourceVersion": []string{}}}, + Response: Response{StatusCode: 200}, + } + _, err := c.Setup().ResourceQuotas(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), "") + c.Validate(t, nil, err) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/restclient.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/restclient.go index a481e83fabd8..55e01aca9282 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/restclient.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/restclient.go @@ -92,7 +92,7 @@ func (c *RESTClient) Verb(verb string) *Request { // if c.Client != nil { // timeout = c.Client.Timeout // } - return NewRequest(c.Client, verb, c.baseURL, c.Codec, c.LegacyBehavior, c.LegacyBehavior).Timeout(c.Timeout) + return NewRequest(c.Client, verb, c.baseURL, c.apiVersion, c.Codec, c.LegacyBehavior, c.LegacyBehavior).Timeout(c.Timeout) } // Post begins a POST request. Short for c.Verb("POST"). @@ -105,6 +105,11 @@ func (c *RESTClient) Put() *Request { return c.Verb("PUT") } +// Patch begins a PATCH request. Short for c.Verb("Patch"). +func (c *RESTClient) Patch() *Request { + return c.Verb("PATCH") +} + // Get begins a GET request. Short for c.Verb("GET"). func (c *RESTClient) Get() *Request { return c.Verb("GET") diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/secrets.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/secrets.go index 8dfd7ccdea45..658190648386 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/secrets.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/secrets.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -33,9 +34,9 @@ type SecretsInterface interface { Create(secret *api.Secret) (*api.Secret, error) Update(secret *api.Secret) (*api.Secret, error) Delete(name string) error - List(label, field labels.Selector) (*api.SecretList, error) + List(label labels.Selector, field fields.Selector) (*api.SecretList, error) Get(name string) (*api.Secret, error) - Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } // events implements Secrets interface @@ -69,14 +70,14 @@ func (s *secrets) Create(secret *api.Secret) (*api.Secret, error) { } // List returns a list of secrets matching the selectors. -func (s *secrets) List(label, field labels.Selector) (*api.SecretList, error) { +func (s *secrets) List(label labels.Selector, field fields.Selector) (*api.SecretList, error) { result := &api.SecretList{} err := s.client.Get(). Namespace(s.namespace). Resource("secrets"). - SelectorParam("labels", label). - SelectorParam("fields", field). + LabelsSelectorParam(api.LabelSelectorQueryParam(s.client.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(s.client.APIVersion()), field). Do(). Into(result) @@ -101,14 +102,14 @@ func (s *secrets) Get(name string) (*api.Secret, error) { } // Watch starts watching for secrets matching the given selectors. -func (s *secrets) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (s *secrets) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { return s.client.Get(). Prefix("watch"). Namespace(s.namespace). Resource("secrets"). Param("resourceVersion", resourceVersion). - SelectorParam("labels", label). - SelectorParam("fields", field). + LabelsSelectorParam(api.LabelSelectorQueryParam(s.client.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(s.client.APIVersion()), field). Watch() } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/services.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/services.go index 15c39fa9ce48..e1b2054cf986 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/services.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/client/services.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -37,7 +38,7 @@ type ServiceInterface interface { Create(srv *api.Service) (*api.Service, error) Update(srv *api.Service) (*api.Service, error) Delete(name string) error - Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } // services implements PodsNamespacer interface @@ -54,7 +55,12 @@ func newServices(c *Client, namespace string) *services { // List takes a selector, and returns the list of services that match that selector func (c *services) List(selector labels.Selector) (result *api.ServiceList, err error) { result = &api.ServiceList{} - err = c.r.Get().Namespace(c.ns).Resource("services").SelectorParam("labels", selector).Do().Into(result) + err = c.r.Get(). + Namespace(c.ns). + Resource("services"). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), selector). + Do(). + Into(result) return } @@ -72,7 +78,13 @@ func (c *services) Get(name string) (result *api.Service, err error) { // Create creates a new service. func (c *services) Create(svc *api.Service) (result *api.Service, err error) { result = &api.Service{} - err = c.r.Post().Namespace(c.ns).Resource("services").Body(svc).Do().Into(result) + // v1beta3 does not allow POST without a namespace. + needNamespace := !api.PreV1Beta3(c.r.APIVersion()) + namespace := c.ns + if needNamespace && len(namespace) == 0 { + namespace = api.NamespaceDefault + } + err = c.r.Post().Namespace(namespace).Resource("services").Body(svc).Do().Into(result) return } @@ -83,23 +95,35 @@ func (c *services) Update(svc *api.Service) (result *api.Service, err error) { err = fmt.Errorf("invalid update object, missing resource version: %v", svc) return } - err = c.r.Put().Namespace(c.ns).Resource("services").Name(svc.Name).Body(svc).Do().Into(result) + // v1beta3 does not allow PUT without a namespace. + needNamespace := !api.PreV1Beta3(c.r.APIVersion()) + namespace := c.ns + if needNamespace && len(namespace) == 0 { + namespace = api.NamespaceDefault + } + err = c.r.Put().Namespace(namespace).Resource("services").Name(svc.Name).Body(svc).Do().Into(result) return } // Delete deletes an existing service. func (c *services) Delete(name string) error { + // v1beta3 does not allow DELETE without a namespace. + needNamespace := !api.PreV1Beta3(c.r.APIVersion()) + namespace := c.ns + if needNamespace && len(namespace) == 0 { + namespace = api.NamespaceDefault + } return c.r.Delete().Namespace(c.ns).Resource("services").Name(name).Do().Error() } // Watch returns a watch.Interface that watches the requested services. -func (c *services) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { +func (c *services) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { return c.r.Get(). Prefix("watch"). Namespace(c.ns). Resource("services"). Param("resourceVersion", resourceVersion). - SelectorParam("labels", label). - SelectorParam("fields", field). + LabelsSelectorParam(api.LabelSelectorQueryParam(c.r.APIVersion()), label). + FieldsSelectorParam(api.FieldSelectorQueryParam(c.r.APIVersion()), field). Watch() } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws/aws.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws/aws.go index fda91ee960eb..c740a15afd68 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws/aws.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws/aws.go @@ -21,31 +21,82 @@ import ( "io" "net" "regexp" + "strings" "code.google.com/p/gcfg" "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/ec2" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" + + "github.com/golang/glog" ) +// Abstraction over EC2, to allow mocking/other implementations type EC2 interface { - Instances(instIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) + // Query EC2 for instances matching the filter + Instances(instIds []string, filter *ec2InstanceFilter) (resp *ec2.InstancesResp, err error) + + // Query the EC2 metadata service (used to discover instance-id etc) + GetMetaData(key string) ([]byte, error) } // AWSCloud is an implementation of Interface, TCPLoadBalancer and Instances for Amazon Web Services. type AWSCloud struct { - ec2 EC2 - cfg *AWSCloudConfig + ec2 EC2 + cfg *AWSCloudConfig + availabilityZone string + region aws.Region } type AWSCloudConfig struct { Global struct { + // TODO: Is there any use for this? We can get it from the instance metadata service Region string } } +// Similar to ec2.Filter, but the filter values can be read from tests +// (ec2.Filter only has private members) +type ec2InstanceFilter struct { + PrivateDNSName string +} + +// True if the passed instance matches the filter +func (f *ec2InstanceFilter) Matches(instance ec2.Instance) bool { + if f.PrivateDNSName != "" && instance.PrivateDNSName != f.PrivateDNSName { + return false + } + return true +} + +// goamzEC2 is an implementation of the EC2 interface, backed by goamz +type GoamzEC2 struct { + ec2 *ec2.EC2 +} + +// Implementation of EC2.Instances +func (self *GoamzEC2) Instances(instanceIds []string, filter *ec2InstanceFilter) (resp *ec2.InstancesResp, err error) { + var goamzFilter *ec2.Filter + if filter != nil { + goamzFilter = ec2.NewFilter() + if filter.PrivateDNSName != "" { + goamzFilter.Add("private-dns-name", filter.PrivateDNSName) + } + } + return self.ec2.Instances(instanceIds, goamzFilter) +} + +func (self *GoamzEC2) GetMetaData(key string) ([]byte, error) { + v, err := aws.GetMetaData(key) + if err != nil { + return nil, fmt.Errorf("Error querying AWS metadata for key %s: %v", key, err) + } + return v, nil +} + type AuthFunc func() (auth aws.Auth, err error) func init() { @@ -89,18 +140,36 @@ func newAWSCloud(config io.Reader, authFunc AuthFunc) (*AWSCloud, error) { return nil, err } + // TODO: We can get the region very easily from the instance-metadata service region, ok := aws.Regions[cfg.Global.Region] if !ok { return nil, fmt.Errorf("not a valid AWS region: %s", cfg.Global.Region) } - ec2 := ec2.New(auth, region) return &AWSCloud{ - ec2: ec2, - cfg: cfg, + ec2: &GoamzEC2{ec2: ec2.New(auth, region)}, + cfg: cfg, + region: region, }, nil } +func (self *AWSCloud) getAvailabilityZone() (string, error) { + // TODO: Do we need sync.Mutex here? + availabilityZone := self.availabilityZone + if self.availabilityZone == "" { + availabilityZoneBytes, err := self.ec2.GetMetaData("placement/availability-zone") + if err != nil { + return "", err + } + if availabilityZoneBytes == nil || len(availabilityZoneBytes) == 0 { + return "", fmt.Errorf("Unable to determine availability-zone from instance metadata") + } + availabilityZone = string(availabilityZoneBytes) + self.availabilityZone = availabilityZone + } + return availabilityZone, nil +} + func (aws *AWSCloud) Clusters() (cloudprovider.Clusters, bool) { return nil, false } @@ -117,37 +186,81 @@ func (aws *AWSCloud) Instances() (cloudprovider.Instances, bool) { // Zones returns an implementation of Zones for Amazon Web Services. func (aws *AWSCloud) Zones() (cloudprovider.Zones, bool) { - return nil, false + return aws, true +} + +// NodeAddresses is an implementation of Instances.NodeAddresses. +func (aws *AWSCloud) NodeAddresses(name string) ([]api.NodeAddress, error) { + inst, err := aws.getInstancesByDnsName(name) + if err != nil { + return nil, err + } + ip := net.ParseIP(inst.PrivateIpAddress) + if ip == nil { + return nil, fmt.Errorf("invalid network IP: %s", inst.PrivateIpAddress) + } + + return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: ip.String()}}, nil +} + +// ExternalID returns the cloud provider ID of the specified instance. +func (aws *AWSCloud) ExternalID(name string) (string, error) { + inst, err := aws.getInstancesByDnsName(name) + if err != nil { + return "", err + } + return inst.InstanceId, nil } -// IPAddress is an implementation of Instances.IPAddress. -func (aws *AWSCloud) IPAddress(name string) (net.IP, error) { - f := ec2.NewFilter() - f.Add("private-dns-name", name) +// Return the instances matching the relevant private dns name. +func (aws *AWSCloud) getInstancesByDnsName(name string) (*ec2.Instance, error) { + f := &ec2InstanceFilter{} + f.PrivateDNSName = name resp, err := aws.ec2.Instances(nil, f) if err != nil { return nil, err } - if len(resp.Reservations) == 0 { - return nil, fmt.Errorf("no reservations found for host: %s", name) - } - if len(resp.Reservations) > 1 { - return nil, fmt.Errorf("multiple reservations found for host: %s", name) + + instances := []*ec2.Instance{} + for _, reservation := range resp.Reservations { + for _, instance := range reservation.Instances { + // TODO: Push running logic down into filter? + if !isAlive(&instance) { + continue + } + + if instance.PrivateDNSName != name { + // TODO: Should we warn here? - the filter should have caught this + // (this will happen in the tests if they don't fully mock the EC2 API) + continue + } + + instances = append(instances, &instance) + } } - if len(resp.Reservations[0].Instances) == 0 { + + if len(instances) == 0 { return nil, fmt.Errorf("no instances found for host: %s", name) } - if len(resp.Reservations[0].Instances) > 1 { + if len(instances) > 1 { return nil, fmt.Errorf("multiple instances found for host: %s", name) } + return instances[0], nil +} - ipAddress := resp.Reservations[0].Instances[0].PrivateIpAddress - ip := net.ParseIP(ipAddress) - if ip == nil { - return nil, fmt.Errorf("invalid network IP: %s", ipAddress) +// Check if the instance is alive (running or pending) +// We typically ignore instances that are not alive +func isAlive(instance *ec2.Instance) bool { + switch instance.State.Name { + case "shutting-down", "terminated", "stopping", "stopped": + return false + case "pending", "running": + return true + default: + glog.Errorf("unknown EC2 instance state: %s", instance.State) + return false } - return ip, nil } // Return a list of instances matching regex string. @@ -160,6 +273,11 @@ func (aws *AWSCloud) getInstancesByRegex(regex string) ([]string, error) { return []string{}, fmt.Errorf("no InstanceResp returned") } + if strings.HasPrefix(regex, "'") && strings.HasSuffix(regex, "'") { + glog.Infof("Stripping quotes around regex (%s)", regex) + regex = regex[1 : len(regex)-1] + } + re, err := regexp.Compile(regex) if err != nil { return []string{}, err @@ -168,6 +286,12 @@ func (aws *AWSCloud) getInstancesByRegex(regex string) ([]string, error) { instances := []string{} for _, reservation := range resp.Reservations { for _, instance := range reservation.Instances { + // TODO: Push filtering down into EC2 API filter? + if !isAlive(&instance) { + glog.V(2).Infof("skipping EC2 instance (not alive): %s", instance.InstanceId) + continue + } + for _, tag := range instance.Tags { if tag.Key == "Name" && re.MatchString(tag.Value) { instances = append(instances, instance.PrivateDNSName) @@ -176,6 +300,7 @@ func (aws *AWSCloud) getInstancesByRegex(regex string) ([]string, error) { } } } + glog.V(2).Infof("Matched EC2 instances: %s", instances) return instances, nil } @@ -185,6 +310,169 @@ func (aws *AWSCloud) List(filter string) ([]string, error) { return aws.getInstancesByRegex(filter) } -func (v *AWSCloud) GetNodeResources(name string) (*api.NodeResources, error) { - return nil, nil +// GetNodeResources implements Instances.GetNodeResources +func (aws *AWSCloud) GetNodeResources(name string) (*api.NodeResources, error) { + instance, err := aws.getInstancesByDnsName(name) + if err != nil { + return nil, err + } + + resources, err := getResourcesByInstanceType(instance.InstanceType) + if err != nil { + return nil, err + } + + return resources, nil +} + +// Builds an api.NodeResources +// cpu is in ecus, memory is in GiB +// We pass the family in so that we could provide more info (e.g. GPU or not) +func makeNodeResources(family string, cpu float64, memory float64) (*api.NodeResources, error) { + return &api.NodeResources{ + Capacity: api.ResourceList{ + api.ResourceCPU: *resource.NewMilliQuantity(int64(cpu*1000), resource.DecimalSI), + api.ResourceMemory: *resource.NewQuantity(int64(memory*1024*1024*1024), resource.BinarySI), + }, + }, nil +} + +// Maps an EC2 instance type to k8s resource information +func getResourcesByInstanceType(instanceType string) (*api.NodeResources, error) { + // There is no API for this (that I know of) + switch instanceType { + // t2: Burstable + // TODO: The ECUs are fake values (because they are burstable), so this is just a guess... + case "t1.micro": + return makeNodeResources("t1", 0.125, 0.615) + + // t2: Burstable + // TODO: The ECUs are fake values (because they are burstable), so this is just a guess... + case "t2.micro": + return makeNodeResources("t2", 0.25, 1) + case "t2.small": + return makeNodeResources("t2", 0.5, 2) + case "t2.medium": + return makeNodeResources("t2", 1, 4) + + // c1: Compute optimized + case "c1.medium": + return makeNodeResources("c1", 5, 1.7) + case "c1.xlarge": + return makeNodeResources("c1", 20, 7) + + // cc2: Compute optimized + case "cc2.8xlarge": + return makeNodeResources("cc2", 88, 60.5) + + // cg1: GPU instances + case "cg1.4xlarge": + return makeNodeResources("cg1", 33.5, 22.5) + + // cr1: Memory optimized + case "cr1.8xlarge": + return makeNodeResources("cr1", 88, 244) + + // c3: Compute optimized + case "c3.large": + return makeNodeResources("c3", 7, 3.75) + case "c3.xlarge": + return makeNodeResources("c3", 14, 7.5) + case "c3.2xlarge": + return makeNodeResources("c3", 28, 15) + case "c3.4xlarge": + return makeNodeResources("c3", 55, 30) + case "c3.8xlarge": + return makeNodeResources("c3", 108, 60) + + // c4: Compute optimized + case "c4.large": + return makeNodeResources("c4", 8, 3.75) + case "c4.xlarge": + return makeNodeResources("c4", 16, 7.5) + case "c4.2xlarge": + return makeNodeResources("c4", 31, 15) + case "c4.4xlarge": + return makeNodeResources("c4", 62, 30) + case "c4.8xlarge": + return makeNodeResources("c4", 132, 60) + + // g2: GPU instances + case "g2.2xlarge": + return makeNodeResources("g2", 26, 15) + + // hi1: Storage optimized (SSD) + case "hi1.4xlarge": + return makeNodeResources("hs1", 35, 60.5) + + // hs1: Storage optimized (HDD) + case "hs1.8xlarge": + return makeNodeResources("hs1", 35, 117) + + // m1: General purpose + case "m1.small": + return makeNodeResources("m1", 1, 1.7) + case "m1.medium": + return makeNodeResources("m1", 2, 3.75) + case "m1.large": + return makeNodeResources("m1", 4, 7.5) + case "m1.xlarge": + return makeNodeResources("m1", 8, 15) + + // m2: Memory optimized + case "m2.xlarge": + return makeNodeResources("m2", 6.5, 17.1) + case "m2.2xlarge": + return makeNodeResources("m2", 13, 34.2) + case "m2.4xlarge": + return makeNodeResources("m2", 26, 68.4) + + // m3: General purpose + case "m3.medium": + return makeNodeResources("m3", 3, 3.75) + case "m3.large": + return makeNodeResources("m3", 6.5, 7.5) + case "m3.xlarge": + return makeNodeResources("m3", 13, 15) + case "m3.2xlarge": + return makeNodeResources("m3", 26, 30) + + // i2: Storage optimized (SSD) + case "i2.xlarge": + return makeNodeResources("i2", 14, 30.5) + case "i2.2xlarge": + return makeNodeResources("i2", 27, 61) + case "i2.4xlarge": + return makeNodeResources("i2", 53, 122) + case "i2.8xlarge": + return makeNodeResources("i2", 104, 244) + + // r3: Memory optimized + case "r3.large": + return makeNodeResources("r3", 6.5, 15) + case "r3.xlarge": + return makeNodeResources("r3", 13, 30.5) + case "r3.2xlarge": + return makeNodeResources("r3", 26, 61) + case "r3.4xlarge": + return makeNodeResources("r3", 52, 122) + case "r3.8xlarge": + return makeNodeResources("r3", 104, 244) + + default: + glog.Errorf("unknown instanceType: %s", instanceType) + return nil, nil + } +} + +// GetZone implements Zones.GetZone +func (self *AWSCloud) GetZone() (cloudprovider.Zone, error) { + availabilityZone, err := self.getAvailabilityZone() + if err != nil { + return cloudprovider.Zone{}, err + } + return cloudprovider.Zone{ + FailureDomain: availabilityZone, + Region: self.region.Name, + }, nil } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws/aws_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws/aws_test.go index 778de9cdf47a..1cef17c91b63 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws/aws_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws/aws_test.go @@ -23,6 +23,9 @@ import ( "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/ec2" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" ) func TestReadAWSCloudConfig(t *testing.T) { @@ -76,34 +79,63 @@ func TestNewAWSCloud(t *testing.T) { } type FakeEC2 struct { - instances func(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) + instances []ec2.Instance + availabilityZone string } -func (ec2 *FakeEC2) Instances(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) { - return ec2.instances(instanceIds, filter) +func (self *FakeEC2) Instances(instanceIds []string, filter *ec2InstanceFilter) (resp *ec2.InstancesResp, err error) { + matches := []ec2.Instance{} + for _, instance := range self.instances { + if filter == nil || filter.Matches(instance) { + matches = append(matches, instance) + } + } + return &ec2.InstancesResp{"", + []ec2.Reservation{ + {"", "", "", nil, matches}}}, nil +} + +func (self *FakeEC2) GetMetaData(key string) ([]byte, error) { + if key == "placement/availability-zone" { + return []byte(self.availabilityZone), nil + } else { + return nil, nil + } } func mockInstancesResp(instances []ec2.Instance) (aws *AWSCloud) { + availabilityZone := "us-west-2d" return &AWSCloud{ - &FakeEC2{ - func(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) { - return &ec2.InstancesResp{"", - []ec2.Reservation{ - {"", "", "", nil, instances}}}, nil - }}, - nil} + ec2: &FakeEC2{ + instances: instances, + availabilityZone: availabilityZone, + }, + } +} + +func mockAvailabilityZone(region string, availabilityZone string) *AWSCloud { + return &AWSCloud{ + ec2: &FakeEC2{ + availabilityZone: availabilityZone, + }, + region: aws.Regions[region], + } } func TestList(t *testing.T) { instances := make([]ec2.Instance, 4) instances[0].Tags = []ec2.Tag{{"Name", "foo"}} instances[0].PrivateDNSName = "instance1" + instances[0].State.Name = "running" instances[1].Tags = []ec2.Tag{{"Name", "bar"}} instances[1].PrivateDNSName = "instance2" + instances[1].State.Name = "running" instances[2].Tags = []ec2.Tag{{"Name", "baz"}} instances[2].PrivateDNSName = "instance3" + instances[2].State.Name = "running" instances[3].Tags = []ec2.Tag{{"Name", "quux"}} instances[3].PrivateDNSName = "instance4" + instances[3].State.Name = "running" aws := mockInstancesResp(instances) @@ -127,31 +159,107 @@ func TestList(t *testing.T) { } } -func TestIPAddress(t *testing.T) { +func TestNodeAddresses(t *testing.T) { + // Note these instances have the same name + // (we test that this produces an error) instances := make([]ec2.Instance, 2) instances[0].PrivateDNSName = "instance1" instances[0].PrivateIpAddress = "192.168.0.1" - instances[1].PrivateDNSName = "instance2" + instances[0].State.Name = "running" + instances[1].PrivateDNSName = "instance1" instances[1].PrivateIpAddress = "192.168.0.2" + instances[1].State.Name = "running" aws1 := mockInstancesResp([]ec2.Instance{}) - _, err1 := aws1.IPAddress("instance") + _, err1 := aws1.NodeAddresses("instance") if err1 == nil { t.Errorf("Should error when no instance found") } aws2 := mockInstancesResp(instances) - _, err2 := aws2.IPAddress("instance1") + _, err2 := aws2.NodeAddresses("instance1") if err2 == nil { t.Errorf("Should error when multiple instances found") } aws3 := mockInstancesResp(instances[0:1]) - ip3, err3 := aws3.IPAddress("instance1") + addrs3, err3 := aws3.NodeAddresses("instance1") if err3 != nil { t.Errorf("Should not error when instance found") } - if e, a := instances[0].PrivateIpAddress, ip3.String(); e != a { + if len(addrs3) != 1 { + t.Errorf("Should return exactly one NodeAddress") + } + if e, a := instances[0].PrivateIpAddress, addrs3[0].Address; e != a { t.Errorf("Expected %v, got %v", e, a) } } + +func TestGetRegion(t *testing.T) { + aws := mockAvailabilityZone("us-west-2", "us-west-2e") + zones, ok := aws.Zones() + if !ok { + t.Fatalf("Unexpected missing zones impl") + } + zone, err := zones.GetZone() + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if zone.Region != "us-west-2" { + t.Errorf("Unexpected region: %s", zone.Region) + } + if zone.FailureDomain != "us-west-2e" { + t.Errorf("Unexpected FailureDomain: %s", zone.FailureDomain) + } +} + +func TestGetResources(t *testing.T) { + instances := make([]ec2.Instance, 3) + instances[0].PrivateDNSName = "m3.medium" + instances[0].InstanceType = "m3.medium" + instances[0].State.Name = "running" + instances[1].PrivateDNSName = "r3.8xlarge" + instances[1].InstanceType = "r3.8xlarge" + instances[1].State.Name = "running" + instances[2].PrivateDNSName = "unknown.type" + instances[2].InstanceType = "unknown.type" + instances[2].State.Name = "running" + + aws1 := mockInstancesResp(instances) + + res1, err1 := aws1.GetNodeResources("m3.medium") + if err1 != nil { + t.Errorf("Should not error when instance type found: %v", err1) + } + e1 := &api.NodeResources{ + Capacity: api.ResourceList{ + api.ResourceCPU: *resource.NewMilliQuantity(int64(3.0*1000), resource.DecimalSI), + api.ResourceMemory: *resource.NewQuantity(int64(3.75*1024*1024*1024), resource.BinarySI), + }, + } + if !reflect.DeepEqual(e1, res1) { + t.Errorf("Expected %v, got %v", e1, res1) + } + + res2, err2 := aws1.GetNodeResources("r3.8xlarge") + if err2 != nil { + t.Errorf("Should not error when instance type found: %v", err2) + } + e2 := &api.NodeResources{ + Capacity: api.ResourceList{ + api.ResourceCPU: *resource.NewMilliQuantity(int64(104.0*1000), resource.DecimalSI), + api.ResourceMemory: *resource.NewQuantity(int64(244.0*1024*1024*1024), resource.BinarySI), + }, + } + if !reflect.DeepEqual(e2, res2) { + t.Errorf("Expected %v, got %v", e2, res2) + } + + res3, err3 := aws1.GetNodeResources("unknown.type") + if err3 != nil { + t.Errorf("Should not error when unknown instance type") + } + if res3 != nil { + t.Errorf("Should return nil resources when unknown instance type") + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/cloud.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/cloud.go index 5ba83617940a..7aa1ff7ab112 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/cloud.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/cloud.go @@ -47,8 +47,8 @@ type TCPLoadBalancer interface { // TCPLoadBalancerExists returns whether the specified load balancer exists. // TODO: Break this up into different interfaces (LB, etc) when we have more than one type of service TCPLoadBalancerExists(name, region string) (bool, error) - // CreateTCPLoadBalancer creates a new tcp load balancer. Returns the IP address of the balancer - CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (net.IP, error) + // CreateTCPLoadBalancer creates a new tcp load balancer. Returns the IP address or hostname of the balancer + CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) // UpdateTCPLoadBalancer updates hosts under the specified load balancer. UpdateTCPLoadBalancer(name, region string, hosts []string) error // DeleteTCPLoadBalancer deletes a specified load balancer. @@ -57,8 +57,10 @@ type TCPLoadBalancer interface { // Instances is an abstract, pluggable interface for sets of instances. type Instances interface { - // IPAddress returns an IP address of the specified instance. - IPAddress(name string) (net.IP, error) + // NodeAddresses returns the addresses of the specified instance. + NodeAddresses(name string) ([]api.NodeAddress, error) + // ExternalID returns the cloud provider ID of the specified instance. + ExternalID(name string) (string, error) // List lists instances that match 'filter' which is a regular expression which must match the entire instance name (fqdn) List(filter string) ([]string, error) // GetNodeResources gets the resources for a particular node diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller/nodecontroller.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller/nodecontroller.go index 2ac4fbfa650f..696a1f220838 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller/nodecontroller.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller/nodecontroller.go @@ -46,9 +46,10 @@ type NodeController struct { staticResources *api.NodeResources nodes []string kubeClient client.Interface - kubeletClient client.KubeletHealthChecker + kubeletClient client.KubeletClient registerRetryCount int podEvictionTimeout time.Duration + lookupIP func(host string) ([]net.IP, error) } // NewNodeController returns a new node controller to sync instances from cloudprovider. @@ -60,7 +61,7 @@ func NewNodeController( nodes []string, staticResources *api.NodeResources, kubeClient client.Interface, - kubeletClient client.KubeletHealthChecker, + kubeletClient client.KubeletClient, registerRetryCount int, podEvictionTimeout time.Duration) *NodeController { return &NodeController{ @@ -72,18 +73,26 @@ func NewNodeController( kubeletClient: kubeletClient, registerRetryCount: registerRetryCount, podEvictionTimeout: podEvictionTimeout, + lookupIP: net.LookupIP, } } // Run creates initial node list and start syncing instances from cloudprovider if any. // It also starts syncing cluster node status. -func (s *NodeController) Run(period time.Duration, syncNodeList bool) { +// 1. RegisterNodes() is called only once to register all initial nodes (from cloudprovider +// or from command line flag). To make cluster bootstrap faster, node controller populates +// node addresses. +// 2. SyncCloud() is called periodically (if enabled) to sync instances from cloudprovider. +// Node created here will only have specs. +// 3. SyncNodeStatus() is called periodically (if enabled) to sync node status for nodes in +// k8s cluster. +func (s *NodeController) Run(period time.Duration, syncNodeList, syncNodeStatus bool) { // Register intial set of nodes with their status set. var nodes *api.NodeList var err error if s.isRunningCloudProvider() { if syncNodeList { - nodes, err = s.CloudNodes() + nodes, err = s.GetCloudNodesWithSpec() if err != nil { glog.Errorf("Error loading initial node from cloudprovider: %v", err) } @@ -91,18 +100,17 @@ func (s *NodeController) Run(period time.Duration, syncNodeList bool) { nodes = &api.NodeList{} } } else { - nodes, err = s.StaticNodes() + nodes, err = s.GetStaticNodesWithSpec() if err != nil { glog.Errorf("Error loading initial static nodes: %v", err) } } - nodes = s.DoChecks(nodes) - nodes, err = s.PopulateIPs(nodes) + nodes, err = s.PopulateAddresses(nodes) if err != nil { glog.Errorf("Error getting nodes ips: %v", err) } if err = s.RegisterNodes(nodes, s.registerRetryCount, period); err != nil { - glog.Errorf("Error registrying node list %+v: %v", nodes, err) + glog.Errorf("Error registering node list %+v: %v", nodes, err) } // Start syncing node list from cloudprovider. @@ -114,12 +122,21 @@ func (s *NodeController) Run(period time.Duration, syncNodeList bool) { }, period) } - // Start syncing node status. - go util.Forever(func() { - if err = s.SyncNodeStatus(); err != nil { - glog.Errorf("Error syncing status: %v", err) - } - }, period) + if syncNodeStatus { + // Start syncing node status. + go util.Forever(func() { + if err = s.SyncNodeStatus(); err != nil { + glog.Errorf("Error syncing status: %v", err) + } + }, period) + } else { + // Start checking node reachability and evicting timeouted pods. + go util.Forever(func() { + if err = s.EvictTimeoutedPods(); err != nil { + glog.Errorf("Error evicting timeouted pods: %v", err) + } + }, period) + } } // RegisterNodes registers the given list of nodes, it keeps retrying for `retryCount` times. @@ -127,6 +144,7 @@ func (s *NodeController) RegisterNodes(nodes *api.NodeList, retryCount int, retr if len(nodes.Items) == 0 { return nil } + registered := util.NewStringSet() nodes = s.canonicalizeName(nodes) for i := 0; i < retryCount; i++ { @@ -157,7 +175,7 @@ func (s *NodeController) RegisterNodes(nodes *api.NodeList, retryCount int, retr // SyncCloud synchronizes the list of instances from cloudprovider to master server. func (s *NodeController) SyncCloud() error { - matches, err := s.CloudNodes() + matches, err := s.GetCloudNodesWithSpec() if err != nil { return err } @@ -166,7 +184,8 @@ func (s *NodeController) SyncCloud() error { return err } nodeMap := make(map[string]*api.Node) - for _, node := range nodes.Items { + for i := range nodes.Items { + node := nodes.Items[i] nodeMap[node.Name] = &node } @@ -176,7 +195,7 @@ func (s *NodeController) SyncCloud() error { glog.Infof("Create node in registry: %s", node.Name) _, err = s.kubeClient.Nodes().Create(&node) if err != nil { - glog.Errorf("Create node error: %s", node.Name) + glog.Errorf("Create node %s error: %v", node.Name, err) } } delete(nodeMap, node.Name) @@ -187,7 +206,7 @@ func (s *NodeController) SyncCloud() error { glog.Infof("Delete node from registry: %s", nodeID) err = s.kubeClient.Nodes().Delete(nodeID) if err != nil { - glog.Errorf("Delete node error: %s", nodeID) + glog.Errorf("Delete node %s error: %v", nodeID, err) } s.deletePods(nodeID) } @@ -201,8 +220,8 @@ func (s *NodeController) SyncNodeStatus() error { if err != nil { return err } - nodes = s.DoChecks(nodes) - nodes, err = s.PopulateIPs(nodes) + nodes = s.UpdateNodesStatus(nodes) + nodes, err = s.PopulateAddresses(nodes) if err != nil { return err } @@ -219,8 +238,35 @@ func (s *NodeController) SyncNodeStatus() error { return nil } -// PopulateIPs queries IPs for given list of nodes. -func (s *NodeController) PopulateIPs(nodes *api.NodeList) (*api.NodeList, error) { +// EvictTimeoutedPods verifies if nodes are reachable by checking the time of last probe +// and deletes pods from not reachable nodes. +func (s *NodeController) EvictTimeoutedPods() error { + nodes, err := s.kubeClient.Nodes().List() + if err != nil { + return err + } + for _, node := range nodes.Items { + if util.Now().After(latestReadyTime(&node).Add(s.podEvictionTimeout)) { + s.deletePods(node.Name) + } + } + return nil +} + +func latestReadyTime(node *api.Node) util.Time { + readyTime := node.ObjectMeta.CreationTimestamp + for _, condition := range node.Status.Conditions { + if condition.Type == api.NodeReady && + condition.Status == api.ConditionFull && + condition.LastProbeTime.After(readyTime.Time) { + readyTime = condition.LastProbeTime + } + } + return readyTime +} + +// PopulateAddresses queries Address for given list of nodes. +func (s *NodeController) PopulateAddresses(nodes *api.NodeList) (*api.NodeList, error) { if s.isRunningCloudProvider() { instances, ok := s.cloud.Instances() if !ok { @@ -228,11 +274,11 @@ func (s *NodeController) PopulateIPs(nodes *api.NodeList) (*api.NodeList, error) } for i := range nodes.Items { node := &nodes.Items[i] - hostIP, err := instances.IPAddress(node.Name) + nodeAddresses, err := instances.NodeAddresses(node.Name) if err != nil { - glog.Errorf("error getting instance ip address for %s: %v", node.Name, err) + glog.Errorf("error getting instance addresses for %s: %v", node.Name, err) } else { - node.Status.HostIP = hostIP.String() + node.Status.Addresses = nodeAddresses } } } else { @@ -240,15 +286,17 @@ func (s *NodeController) PopulateIPs(nodes *api.NodeList) (*api.NodeList, error) node := &nodes.Items[i] addr := net.ParseIP(node.Name) if addr != nil { - node.Status.HostIP = node.Name + address := api.NodeAddress{Type: api.NodeLegacyHostIP, Address: addr.String()} + node.Status.Addresses = []api.NodeAddress{address} } else { - addrs, err := net.LookupIP(node.Name) + addrs, err := s.lookupIP(node.Name) if err != nil { glog.Errorf("Can't get ip address of node %s: %v", node.Name, err) } else if len(addrs) == 0 { glog.Errorf("No ip address for node %v", node.Name) } else { - node.Status.HostIP = addrs[0].String() + address := api.NodeAddress{Type: api.NodeLegacyHostIP, Address: addrs[0].String()} + node.Status.Addresses = []api.NodeAddress{address} } } } @@ -256,13 +304,16 @@ func (s *NodeController) PopulateIPs(nodes *api.NodeList) (*api.NodeList, error) return nodes, nil } -// DoChecks performs health checking for given list of nodes. -func (s *NodeController) DoChecks(nodes *api.NodeList) *api.NodeList { +// UpdateNodesStatus performs various condition checks for given list of nodes. +func (s *NodeController) UpdateNodesStatus(nodes *api.NodeList) *api.NodeList { var wg sync.WaitGroup wg.Add(len(nodes.Items)) for i := range nodes.Items { go func(node *api.Node) { node.Status.Conditions = s.DoCheck(node) + if err := s.updateNodeInfo(node); err != nil { + glog.Errorf("Can't collect information for node %s: %v", node.Name, err) + } wg.Done() }(&nodes.Items[i]) } @@ -270,21 +321,26 @@ func (s *NodeController) DoChecks(nodes *api.NodeList) *api.NodeList { return nodes } -// DoCheck performs health checking for given node. +func (s *NodeController) updateNodeInfo(node *api.Node) error { + nodeInfo, err := s.kubeletClient.GetNodeInfo(node.Name) + if err != nil { + return err + } + for key, value := range nodeInfo.Capacity { + node.Spec.Capacity[key] = value + } + node.Status.NodeInfo = nodeInfo.NodeSystemInfo + return nil +} + +// DoCheck performs various condition checks for given node. func (s *NodeController) DoCheck(node *api.Node) []api.NodeCondition { var conditions []api.NodeCondition // Check Condition: NodeReady. TODO: More node conditions. oldReadyCondition := s.getCondition(node, api.NodeReady) newReadyCondition := s.checkNodeReady(node) - if oldReadyCondition != nil && oldReadyCondition.Status == newReadyCondition.Status { - // If node status doesn't change, transition time is same as last time. - newReadyCondition.LastTransitionTime = oldReadyCondition.LastTransitionTime - } else { - // Set transition time to Now() if node status changes or `oldReadyCondition` is nil, which - // happens only when the node is checked for the first time. - newReadyCondition.LastTransitionTime = util.Now() - } + s.updateLastTransitionTime(oldReadyCondition, newReadyCondition) if newReadyCondition.Status != api.ConditionFull { // Node is not ready for this probe, we need to check if pods need to be deleted. @@ -295,33 +351,69 @@ func (s *NodeController) DoCheck(node *api.Node) []api.NodeCondition { s.deletePods(node.Name) } } - conditions = append(conditions, *newReadyCondition) + // Check Condition: NodeSchedulable + oldSchedulableCondition := s.getCondition(node, api.NodeSchedulable) + newSchedulableCondition := s.checkNodeSchedulable(node) + s.updateLastTransitionTime(oldSchedulableCondition, newSchedulableCondition) + conditions = append(conditions, *newSchedulableCondition) + return conditions } +// updateLastTransitionTime updates LastTransitionTime for the newCondition based on oldCondition. +func (s *NodeController) updateLastTransitionTime(oldCondition, newCondition *api.NodeCondition) { + if oldCondition != nil && oldCondition.Status == newCondition.Status { + // If node status doesn't change, transition time is same as last time. + newCondition.LastTransitionTime = oldCondition.LastTransitionTime + } else { + // Set transition time to Now() if node status changes or `oldCondition` is nil, which + // happens only when the node is checked for the first time. + newCondition.LastTransitionTime = util.Now() + } +} + +// checkNodeSchedulable checks node schedulable condition, without transition timestamp set. +func (s *NodeController) checkNodeSchedulable(node *api.Node) *api.NodeCondition { + if node.Spec.Unschedulable { + return &api.NodeCondition{ + Type: api.NodeSchedulable, + Status: api.ConditionNone, + Reason: "User marked unschedulable during node create/update", + LastProbeTime: util.Now(), + } + } else { + return &api.NodeCondition{ + Type: api.NodeSchedulable, + Status: api.ConditionFull, + Reason: "Node is schedulable by default", + LastProbeTime: util.Now(), + } + } +} + // checkNodeReady checks raw node ready condition, without transition timestamp set. func (s *NodeController) checkNodeReady(node *api.Node) *api.NodeCondition { switch status, err := s.kubeletClient.HealthCheck(node.Name); { case err != nil: glog.V(2).Infof("NodeController: node %s health check error: %v", node.Name, err) return &api.NodeCondition{ - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionUnknown, Reason: fmt.Sprintf("Node health check error: %v", err), LastProbeTime: util.Now(), } case status == probe.Failure: return &api.NodeCondition{ - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionNone, Reason: fmt.Sprintf("Node health check failed: kubelet /healthz endpoint returns not ok"), LastProbeTime: util.Now(), } default: return &api.NodeCondition{ - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionFull, Reason: fmt.Sprintf("Node health check succeeded: kubelet /healthz endpoint returns ok"), LastProbeTime: util.Now(), @@ -342,17 +434,18 @@ func (s *NodeController) deletePods(nodeID string) error { continue } glog.V(2).Infof("Delete pod %v", pod.Name) - if err := s.kubeClient.Pods(api.NamespaceAll).Delete(pod.Name); err != nil { - glog.Errorf("Error deleting pod %v", pod.Name) + if err := s.kubeClient.Pods(pod.Namespace).Delete(pod.Name); err != nil { + glog.Errorf("Error deleting pod %v: %v", pod.Name, err) } } return nil } -// StaticNodes constructs and returns api.NodeList for static nodes. If error -// occurs, an empty NodeList will be returned with a non-nil error info. -func (s *NodeController) StaticNodes() (*api.NodeList, error) { +// GetStaticNodesWithSpec constructs and returns api.NodeList for static nodes. If error +// occurs, an empty NodeList will be returned with a non-nil error info. The +// method only constructs spec fields for nodes. +func (s *NodeController) GetStaticNodesWithSpec() (*api.NodeList, error) { result := &api.NodeList{} for _, nodeID := range s.nodes { node := api.Node{ @@ -364,9 +457,10 @@ func (s *NodeController) StaticNodes() (*api.NodeList, error) { return result, nil } -// CloudNodes constructs and returns api.NodeList from cloudprovider. If error -// occurs, an empty NodeList will be returned with a non-nil error info. -func (s *NodeController) CloudNodes() (*api.NodeList, error) { +// GetCloudNodesWithSpec constructs and returns api.NodeList from cloudprovider. If error +// occurs, an empty NodeList will be returned with a non-nil error info. The +// method only constructs spec fields for nodes. +func (s *NodeController) GetCloudNodesWithSpec() (*api.NodeList, error) { result := &api.NodeList{} instances, ok := s.cloud.Instances() if !ok { @@ -389,6 +483,12 @@ func (s *NodeController) CloudNodes() (*api.NodeList, error) { if resources != nil { node.Spec.Capacity = resources.Capacity } + instanceID, err := instances.ExternalID(node.Name) + if err != nil { + glog.Errorf("error getting instance id for %s: %v", node.Name, err) + } else { + node.Spec.ExternalID = instanceID + } result.Items = append(result.Items, node) } return result, nil @@ -408,10 +508,10 @@ func (s *NodeController) canonicalizeName(nodes *api.NodeList) *api.NodeList { } // getCondition returns a condition object for the specific condition -// kind, nil if the condition is not set. -func (s *NodeController) getCondition(node *api.Node, kind api.NodeConditionKind) *api.NodeCondition { +// type, nil if the condition is not set. +func (s *NodeController) getCondition(node *api.Node, conditionType api.NodeConditionType) *api.NodeCondition { for i := range node.Status.Conditions { - if node.Status.Conditions[i].Kind == kind { + if node.Status.Conditions[i].Type == conditionType { return &node.Status.Conditions[i] } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller/nodecontroller_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller/nodecontroller_test.go index 9c7abfbfec96..899e71c05523 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller/nodecontroller_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/controller/nodecontroller_test.go @@ -18,6 +18,7 @@ package controller import ( "errors" + "fmt" "net" "reflect" "sort" @@ -123,6 +124,10 @@ func (c *FakeKubeletClient) GetPodStatus(host, podNamespace, podID string) (api. return api.PodStatusResult{}, errors.New("Not Implemented") } +func (c *FakeKubeletClient) GetNodeInfo(host string) (api.NodeInfo, error) { + return api.NodeInfo{}, errors.New("Not Implemented") +} + func (c *FakeKubeletClient) HealthCheck(host string) (probe.Result, error) { return c.Status, c.Err } @@ -247,7 +252,7 @@ func TestRegisterNodes(t *testing.T) { } } -func TestCreateStaticNodes(t *testing.T) { +func TestCreateGetStaticNodesWithSpec(t *testing.T) { table := []struct { machines []string expectedNodes *api.NodeList @@ -289,7 +294,7 @@ func TestCreateStaticNodes(t *testing.T) { for _, item := range table { nodeController := NewNodeController(nil, "", item.machines, &api.NodeResources{}, nil, nil, 10, time.Minute) - nodes, err := nodeController.StaticNodes() + nodes, err := nodeController.GetStaticNodesWithSpec() if err != nil { t.Errorf("unexpected error: %v", err) } @@ -299,7 +304,7 @@ func TestCreateStaticNodes(t *testing.T) { } } -func TestCreateCloudNodes(t *testing.T) { +func TestCreateGetCloudNodesWithSpec(t *testing.T) { resourceList := api.ResourceList{ api.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), api.ResourceMemory: *resource.NewQuantity(3000, resource.DecimalSI), @@ -350,7 +355,7 @@ func TestCreateCloudNodes(t *testing.T) { for _, item := range table { nodeController := NewNodeController(item.fakeCloud, ".*", nil, &api.NodeResources{}, nil, nil, 10, time.Minute) - nodes, err := nodeController.CloudNodes() + nodes, err := nodeController.GetCloudNodesWithSpec() if err != nil { t.Errorf("unexpected error: %v", err) } @@ -366,7 +371,8 @@ func TestSyncCloud(t *testing.T) { fakeCloud *fake_cloud.FakeCloud matchRE string expectedRequestCount int - expectedCreated []string + expectedNameCreated []string + expectedExtIDCreated []string expectedDeleted []string }{ { @@ -376,10 +382,15 @@ func TestSyncCloud(t *testing.T) { }, fakeCloud: &fake_cloud.FakeCloud{ Machines: []string{"node0"}, + ExtID: map[string]string{ + "node0": "ext-node0", + "node1": "ext-node1", + }, }, matchRE: ".*", expectedRequestCount: 1, // List - expectedCreated: []string{}, + expectedNameCreated: []string{}, + expectedExtIDCreated: []string{}, expectedDeleted: []string{}, }, { @@ -389,10 +400,15 @@ func TestSyncCloud(t *testing.T) { }, fakeCloud: &fake_cloud.FakeCloud{ Machines: []string{"node0", "node1"}, + ExtID: map[string]string{ + "node0": "ext-node0", + "node1": "ext-node1", + }, }, matchRE: ".*", expectedRequestCount: 2, // List + Create - expectedCreated: []string{"node1"}, + expectedNameCreated: []string{"node1"}, + expectedExtIDCreated: []string{"ext-node1"}, expectedDeleted: []string{}, }, { @@ -402,10 +418,15 @@ func TestSyncCloud(t *testing.T) { }, fakeCloud: &fake_cloud.FakeCloud{ Machines: []string{"node0"}, + ExtID: map[string]string{ + "node0": "ext-node0", + "node1": "ext-node1", + }, }, matchRE: ".*", expectedRequestCount: 2, // List + Delete - expectedCreated: []string{}, + expectedNameCreated: []string{}, + expectedExtIDCreated: []string{}, expectedDeleted: []string{"node1"}, }, { @@ -415,10 +436,16 @@ func TestSyncCloud(t *testing.T) { }, fakeCloud: &fake_cloud.FakeCloud{ Machines: []string{"node0", "node1", "fake"}, + ExtID: map[string]string{ + "node0": "ext-node0", + "node1": "ext-node1", + "fake": "ext-fake", + }, }, matchRE: "node[0-9]+", expectedRequestCount: 2, // List + Create - expectedCreated: []string{"node1"}, + expectedNameCreated: []string{"node1"}, + expectedExtIDCreated: []string{"ext-node1"}, expectedDeleted: []string{}, }, } @@ -432,8 +459,12 @@ func TestSyncCloud(t *testing.T) { t.Errorf("expected %v call, but got %v.", item.expectedRequestCount, item.fakeNodeHandler.RequestCount) } nodes := sortedNodeNames(item.fakeNodeHandler.CreatedNodes) - if !reflect.DeepEqual(item.expectedCreated, nodes) { - t.Errorf("expected node list %+v, got %+v", item.expectedCreated, nodes) + if !reflect.DeepEqual(item.expectedNameCreated, nodes) { + t.Errorf("expected node list %+v, got %+v", item.expectedNameCreated, nodes) + } + nodeExtIDs := sortedNodeExternalIDs(item.fakeNodeHandler.CreatedNodes) + if !reflect.DeepEqual(item.expectedExtIDCreated, nodeExtIDs) { + t.Errorf("expected node external id list %+v, got %+v", item.expectedExtIDCreated, nodeExtIDs) } nodes = sortedNodeNames(item.fakeNodeHandler.DeletedNodes) if !reflect.DeepEqual(item.expectedDeleted, nodes) { @@ -519,13 +550,15 @@ func TestSyncCloudDeletePods(t *testing.T) { } } -func TestHealthCheckNode(t *testing.T) { +func TestNodeConditionsCheck(t *testing.T) { table := []struct { node *api.Node fakeKubeletClient *FakeKubeletClient expectedConditions []api.NodeCondition }{ { + // Node with default spec and kubelet /healthz probe returns success. + // Expected node condition to be ready and marked schedulable. node: newNode("node0"), fakeKubeletClient: &FakeKubeletClient{ Status: probe.Success, @@ -533,38 +566,57 @@ func TestHealthCheckNode(t *testing.T) { }, expectedConditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionFull, Reason: "Node health check succeeded: kubelet /healthz endpoint returns ok", }, + { + Type: api.NodeSchedulable, + Status: api.ConditionFull, + Reason: "Node is schedulable by default", + }, }, }, { - node: newNode("node0"), + // User specified node as schedulable and kubelet /healthz probe returns failure with no error. + // Expected node condition to be not ready and marked schedulable. + node: &api.Node{ObjectMeta: api.ObjectMeta{Name: "node0"}, Spec: api.NodeSpec{Unschedulable: false}}, fakeKubeletClient: &FakeKubeletClient{ Status: probe.Failure, Err: nil, }, expectedConditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionNone, Reason: "Node health check failed: kubelet /healthz endpoint returns not ok", }, + { + Type: api.NodeSchedulable, + Status: api.ConditionFull, + Reason: "Node is schedulable by default", + }, }, }, { - node: newNode("node0"), + // User specified node as unschedulable and kubelet /healthz probe returns failure with some error. + // Expected node condition to be not ready and marked unschedulable. + node: &api.Node{ObjectMeta: api.ObjectMeta{Name: "node0"}, Spec: api.NodeSpec{Unschedulable: true}}, fakeKubeletClient: &FakeKubeletClient{ Status: probe.Failure, Err: errors.New("Error"), }, expectedConditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionUnknown, Reason: "Node health check error: Error", }, + { + Type: api.NodeSchedulable, + Status: api.ConditionNone, + Reason: "User marked unschedulable during node create/update", + }, }, }, } @@ -588,35 +640,37 @@ func TestHealthCheckNode(t *testing.T) { } } -func TestPopulateNodeIPs(t *testing.T) { +func TestPopulateNodeAddresses(t *testing.T) { table := []struct { - nodes *api.NodeList - fakeCloud *fake_cloud.FakeCloud - expectedFail bool - expectedIP string + nodes *api.NodeList + fakeCloud *fake_cloud.FakeCloud + expectedFail bool + expectedAddresses []api.NodeAddress }{ { - nodes: &api.NodeList{Items: []api.Node{*newNode("node0"), *newNode("node1")}}, - fakeCloud: &fake_cloud.FakeCloud{IP: net.ParseIP("1.2.3.4")}, - expectedIP: "1.2.3.4", + nodes: &api.NodeList{Items: []api.Node{*newNode("node0"), *newNode("node1")}}, + fakeCloud: &fake_cloud.FakeCloud{Addresses: []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: "1.2.3.4"}}}, + expectedAddresses: []api.NodeAddress{ + {Type: api.NodeLegacyHostIP, Address: "1.2.3.4"}, + }, }, { - nodes: &api.NodeList{Items: []api.Node{*newNode("node0"), *newNode("node1")}}, - fakeCloud: &fake_cloud.FakeCloud{Err: ErrQueryIPAddress}, - expectedIP: "", + nodes: &api.NodeList{Items: []api.Node{*newNode("node0"), *newNode("node1")}}, + fakeCloud: &fake_cloud.FakeCloud{Err: ErrQueryIPAddress}, + expectedAddresses: nil, }, } for _, item := range table { nodeController := NewNodeController(item.fakeCloud, ".*", nil, nil, nil, nil, 10, time.Minute) - result, err := nodeController.PopulateIPs(item.nodes) + result, err := nodeController.PopulateAddresses(item.nodes) // In case of IP querying error, we should continue. if err != nil { t.Errorf("unexpected error: %v", err) } for _, node := range result.Items { - if node.Status.HostIP != item.expectedIP { - t.Errorf("expect HostIP %s, got %s", item.expectedIP, node.Status.HostIP) + if !reflect.DeepEqual(item.expectedAddresses, node.Status.Addresses) { + t.Errorf("expect HostIP %s, got %s", item.expectedAddresses, node.Status.Addresses) } } } @@ -630,19 +684,27 @@ func TestSyncNodeStatusTransitionTime(t *testing.T) { expectedTransitionTimeChange bool }{ { - // Existing node is healthy, current porbe is healthy too. + // Existing node is healthy, current probe is healthy too. + // Existing node is schedulable, again explicitly mark node as schedulable. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{ { ObjectMeta: api.ObjectMeta{Name: "node0"}, + Spec: api.NodeSpec{Unschedulable: false}, Status: api.NodeStatus{ Conditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionFull, Reason: "Node health check succeeded: kubelet /healthz endpoint returns ok", LastTransitionTime: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), }, + { + Type: api.NodeSchedulable, + Status: api.ConditionFull, + Reason: "Node is schedulable by default", + LastTransitionTime: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), + }, }, }, }, @@ -656,19 +718,27 @@ func TestSyncNodeStatusTransitionTime(t *testing.T) { expectedTransitionTimeChange: false, }, { - // Existing node is healthy, current porbe is unhealthy. + // Existing node is healthy, current probe is unhealthy. + // Existing node is schedulable, mark node as unschedulable. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{ { ObjectMeta: api.ObjectMeta{Name: "node0"}, + Spec: api.NodeSpec{Unschedulable: true}, Status: api.NodeStatus{ Conditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionFull, Reason: "Node health check succeeded: kubelet /healthz endpoint returns ok", LastTransitionTime: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), }, + { + Type: api.NodeSchedulable, + Status: api.ConditionFull, + Reason: "Node is schedulable by default", + LastTransitionTime: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), + }, }, }, }, @@ -685,6 +755,9 @@ func TestSyncNodeStatusTransitionTime(t *testing.T) { for _, item := range table { nodeController := NewNodeController(nil, "", []string{"node0"}, nil, item.fakeNodeHandler, item.fakeKubeletClient, 10, time.Minute) + nodeController.lookupIP = func(host string) ([]net.IP, error) { + return nil, fmt.Errorf("lookup %v: no such host", host) + } if err := nodeController.SyncNodeStatus(); err != nil { t.Errorf("unexpected error: %v", err) } @@ -709,6 +782,118 @@ func TestSyncNodeStatusTransitionTime(t *testing.T) { } } +func TestEvictTimeoutedPods(t *testing.T) { + table := []struct { + fakeNodeHandler *FakeNodeHandler + expectedRequestCount int + expectedActions []client.FakeAction + }{ + // Node created long time ago, with no status. + { + fakeNodeHandler: &FakeNodeHandler{ + Existing: []*api.Node{ + { + ObjectMeta: api.ObjectMeta{ + Name: "node0", + CreationTimestamp: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + }, + Fake: client.Fake{ + PodsList: api.PodList{Items: []api.Pod{*newPod("pod0", "node0")}}, + }, + }, + expectedRequestCount: 1, // List + expectedActions: []client.FakeAction{{Action: "list-pods"}, {Action: "delete-pod", Value: "pod0"}}, + }, + // Node created recently, with no status. + { + fakeNodeHandler: &FakeNodeHandler{ + Existing: []*api.Node{ + { + ObjectMeta: api.ObjectMeta{ + Name: "node0", + CreationTimestamp: util.Now(), + }, + }, + }, + Fake: client.Fake{ + PodsList: api.PodList{Items: []api.Pod{*newPod("pod0", "node0")}}, + }, + }, + expectedRequestCount: 1, // List + expectedActions: nil, + }, + // Node created long time ago, with status updated long time ago. + { + fakeNodeHandler: &FakeNodeHandler{ + Existing: []*api.Node{ + { + ObjectMeta: api.ObjectMeta{ + Name: "node0", + CreationTimestamp: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), + }, + Status: api.NodeStatus{ + Conditions: []api.NodeCondition{ + { + Type: api.NodeReady, + Status: api.ConditionFull, + LastProbeTime: util.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + }, + }, + }, + Fake: client.Fake{ + PodsList: api.PodList{Items: []api.Pod{*newPod("pod0", "node0")}}, + }, + }, + expectedRequestCount: 1, // List + expectedActions: []client.FakeAction{{Action: "list-pods"}, {Action: "delete-pod", Value: "pod0"}}, + }, + // Node created long time ago, with status updated recently. + { + fakeNodeHandler: &FakeNodeHandler{ + Existing: []*api.Node{ + { + ObjectMeta: api.ObjectMeta{ + Name: "node0", + CreationTimestamp: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), + }, + Status: api.NodeStatus{ + Conditions: []api.NodeCondition{ + { + Type: api.NodeReady, + Status: api.ConditionFull, + LastProbeTime: util.Now(), + }, + }, + }, + }, + }, + Fake: client.Fake{ + PodsList: api.PodList{Items: []api.Pod{*newPod("pod0", "node0")}}, + }, + }, + expectedRequestCount: 1, // List + expectedActions: nil, + }, + } + + for _, item := range table { + nodeController := NewNodeController(nil, "", []string{"node0"}, nil, item.fakeNodeHandler, nil, 10, 5*time.Minute) + if err := nodeController.EvictTimeoutedPods(); err != nil { + t.Errorf("unexpected error: %v", err) + } + if item.expectedRequestCount != item.fakeNodeHandler.RequestCount { + t.Errorf("expected %v call, but got %v.", item.expectedRequestCount, item.fakeNodeHandler.RequestCount) + } + if !reflect.DeepEqual(item.expectedActions, item.fakeNodeHandler.Actions) { + t.Errorf("actions differs, expected %+v, got %+v", item.expectedActions, item.fakeNodeHandler.Actions) + } + } +} + func TestSyncNodeStatusDeletePods(t *testing.T) { table := []struct { fakeNodeHandler *FakeNodeHandler @@ -717,7 +902,7 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { expectedActions []client.FakeAction }{ { - // Existing node is healthy, current porbe is healthy too. + // Existing node is healthy, current probe is healthy too. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{ { @@ -725,7 +910,7 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { Status: api.NodeStatus{ Conditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionFull, Reason: "Node health check succeeded: kubelet /healthz endpoint returns ok", LastTransitionTime: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), @@ -746,7 +931,7 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { expectedActions: nil, }, { - // Existing node is healthy, current porbe is unhealthy, i.e. node just becomes unhealthy. + // Existing node is healthy, current probe is unhealthy, i.e. node just becomes unhealthy. // Do not delete pods. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{ @@ -755,7 +940,7 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { Status: api.NodeStatus{ Conditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionFull, Reason: "Node health check succeeded: kubelet /healthz endpoint returns ok", LastTransitionTime: util.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC), @@ -776,7 +961,7 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { expectedActions: nil, }, { - // Existing node unhealthy, current porbe is unhealthy. Node is still within grace peroid. + // Existing node unhealthy, current probe is unhealthy. Node is still within grace peroid. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{ { @@ -784,7 +969,7 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { Status: api.NodeStatus{ Conditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionNone, Reason: "Node health check failed: kubelet /healthz endpoint returns not ok", // Here, last transition time is Now(). In node controller, the new condition's probe time is @@ -808,7 +993,7 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { expectedActions: nil, }, { - // Existing node unhealthy, current porbe is unhealthy. Node exceeds grace peroid. + // Existing node unhealthy, current probe is unhealthy. Node exceeds grace peroid. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{ { @@ -816,7 +1001,7 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { Status: api.NodeStatus{ Conditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionNone, Reason: "Node health check failed: kubelet /healthz endpoint returns not ok", // Here, last transition time is in the past, and in node controller, the @@ -844,6 +1029,9 @@ func TestSyncNodeStatusDeletePods(t *testing.T) { for _, item := range table { nodeController := NewNodeController(nil, "", []string{"node0"}, nil, item.fakeNodeHandler, item.fakeKubeletClient, 10, 5*time.Minute) + nodeController.lookupIP = func(host string) ([]net.IP, error) { + return nil, fmt.Errorf("lookup %v: no such host", host) + } if err := nodeController.SyncNodeStatus(); err != nil { t.Errorf("unexpected error: %v", err) } @@ -873,7 +1061,7 @@ func TestSyncNodeStatus(t *testing.T) { Err: nil, }, fakeCloud: &fake_cloud.FakeCloud{ - IP: net.ParseIP("1.2.3.4"), + Addresses: []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: "1.2.3.4"}}, }, expectedNodes: []*api.Node{ { @@ -881,12 +1069,19 @@ func TestSyncNodeStatus(t *testing.T) { Status: api.NodeStatus{ Conditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionFull, Reason: "Node health check succeeded: kubelet /healthz endpoint returns ok", }, + { + Type: api.NodeSchedulable, + Status: api.ConditionFull, + Reason: "Node is schedulable by default", + }, + }, + Addresses: []api.NodeAddress{ + {Type: api.NodeLegacyHostIP, Address: "1.2.3.4"}, }, - HostIP: "1.2.3.4", }, }, { @@ -894,12 +1089,19 @@ func TestSyncNodeStatus(t *testing.T) { Status: api.NodeStatus{ Conditions: []api.NodeCondition{ { - Kind: api.NodeReady, + Type: api.NodeReady, Status: api.ConditionFull, Reason: "Node health check succeeded: kubelet /healthz endpoint returns ok", }, + { + Type: api.NodeSchedulable, + Status: api.ConditionFull, + Reason: "Node is schedulable by default", + }, + }, + Addresses: []api.NodeAddress{ + {Type: api.NodeLegacyHostIP, Address: "1.2.3.4"}, }, - HostIP: "1.2.3.4", }, }, }, @@ -959,6 +1161,15 @@ func sortedNodeNames(nodes []*api.Node) []string { return nodeNames } +func sortedNodeExternalIDs(nodes []*api.Node) []string { + nodeExternalIDs := []string{} + for _, node := range nodes { + nodeExternalIDs = append(nodeExternalIDs, node.Spec.ExternalID) + } + sort.Strings(nodeExternalIDs) + return nodeExternalIDs +} + func contains(node *api.Node, nodes []*api.Node) bool { for i := 0; i < len(nodes); i++ { if node.Name == nodes[i].Name { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/fake/fake.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/fake/fake.go index 4efc6d82d780..7aa0b8fc24ff 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/fake/fake.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/fake/fake.go @@ -24,17 +24,28 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" ) +// FakeBalancer is a fake storage of balancer information +type FakeBalancer struct { + Name string + Region string + ExternalIP net.IP + Port int + Hosts []string +} + // FakeCloud is a test-double implementation of Interface, TCPLoadBalancer and Instances. It is useful for testing. type FakeCloud struct { Exists bool Err error Calls []string - IP net.IP + Addresses []api.NodeAddress + ExtID map[string]string Machines []string NodeResources *api.NodeResources ClusterList []string MasterName string ExternalIP net.IP + Balancers []FakeBalancer cloudprovider.Zone } @@ -84,9 +95,10 @@ func (f *FakeCloud) TCPLoadBalancerExists(name, region string) (bool, error) { // CreateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.CreateTCPLoadBalancer. // It adds an entry "create" into the internal method call record. -func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (net.IP, error) { +func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) { f.addCall("create") - return f.ExternalIP, f.Err + f.Balancers = append(f.Balancers, FakeBalancer{name, region, externalIP, port, hosts}) + return f.ExternalIP.String(), f.Err } // UpdateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.UpdateTCPLoadBalancer. @@ -103,11 +115,19 @@ func (f *FakeCloud) DeleteTCPLoadBalancer(name, region string) error { return f.Err } -// IPAddress is a test-spy implementation of Instances.IPAddress. -// It adds an entry "ip-address" into the internal method call record. -func (f *FakeCloud) IPAddress(instance string) (net.IP, error) { - f.addCall("ip-address") - return f.IP, f.Err +// NodeAddresses is a test-spy implementation of Instances.NodeAddresses. +// It adds an entry "node-addresses" into the internal method call record. +func (f *FakeCloud) NodeAddresses(instance string) ([]api.NodeAddress, error) { + f.addCall("node-addresses") + return f.Addresses, f.Err +} + +// ExternalID is a test-spy implementation of Instances.ExternalID. +// It adds an entry "external-id" into the internal method call record. +// It returns an external id to the mapped instance name, if not found, it will return "ext-{instance}" +func (f *FakeCloud) ExternalID(instance string) (string, error) { + f.addCall("external-id") + return f.ExtID[instance], f.Err } // List is a test-spy implementation of Instances.List. diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce/gce.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce/gce.go index 529c810643e5..da36e009db3b 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce/gce.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce/gce.go @@ -17,6 +17,7 @@ limitations under the License. package gce_cloud import ( + "errors" "fmt" "io" "io/ioutil" @@ -174,10 +175,13 @@ func (gce *GCECloud) makeTargetPool(name, region string, hosts []string, affinit Instances: instances, SessionAffinity: string(affinityType), } - _, err := gce.service.TargetPools.Insert(gce.projectID, region, pool).Do() + op, err := gce.service.TargetPools.Insert(gce.projectID, region, pool).Do() if err != nil { return "", err } + if err = gce.waitForRegionOp(op, region); err != nil { + return "", err + } link := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/regions/%s/targetPools/%s", gce.projectID, region, name) return link, nil } @@ -186,12 +190,15 @@ func (gce *GCECloud) waitForRegionOp(op *compute.Operation, region string) error pollOp := op for pollOp.Status != "DONE" { var err error - time.Sleep(time.Second * 10) + time.Sleep(time.Second) pollOp, err = gce.service.RegionOperations.Get(gce.projectID, region, op.Name).Do() if err != nil { return err } } + if pollOp.Error != nil && len(pollOp.Error.Errors) > 0 { + return errors.New(pollOp.Error.Errors[0].Message) + } return nil } @@ -215,10 +222,10 @@ func translateAffinityType(affinityType api.AffinityType) GCEAffinityType { } // CreateTCPLoadBalancer is an implementation of TCPLoadBalancer.CreateTCPLoadBalancer. -func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (net.IP, error) { +func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) { pool, err := gce.makeTargetPool(name, region, hosts, translateAffinityType(affinityType)) if err != nil { - return nil, err + return "", err } req := &compute.ForwardingRule{ Name: name, @@ -231,17 +238,17 @@ func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.I } op, err := gce.service.ForwardingRules.Insert(gce.projectID, region, req).Do() if err != nil { - return nil, err + return "", err } err = gce.waitForRegionOp(op, region) if err != nil { - return nil, err + return "", err } fwd, err := gce.service.ForwardingRules.Get(gce.projectID, region, name).Do() if err != nil { - return nil, err + return "", err } - return net.ParseIP(fwd.IPAddress), nil + return fwd.IPAddress, nil } // UpdateTCPLoadBalancer is an implementation of TCPLoadBalancer.UpdateTCPLoadBalancer. @@ -254,17 +261,35 @@ func (gce *GCECloud) UpdateTCPLoadBalancer(name, region string, hosts []string) Instances: refs, } - _, err := gce.service.TargetPools.AddInstance(gce.projectID, region, name, req).Do() + op, err := gce.service.TargetPools.AddInstance(gce.projectID, region, name, req).Do() + if err != nil { + return err + } + err = gce.waitForRegionOp(op, region) return err } // DeleteTCPLoadBalancer is an implementation of TCPLoadBalancer.DeleteTCPLoadBalancer. func (gce *GCECloud) DeleteTCPLoadBalancer(name, region string) error { - _, err := gce.service.ForwardingRules.Delete(gce.projectID, region, name).Do() + op, err := gce.service.ForwardingRules.Delete(gce.projectID, region, name).Do() if err != nil { + glog.Warningln("Failed to delete Forwarding Rules %s: got error %s. Trying to delete Target Pool", name, err.Error()) return err + } else { + err = gce.waitForRegionOp(op, region) + if err != nil { + glog.Warningln("Failed waiting for Forwarding Rule %s to be deleted: got error %s. Trying to delete Target Pool", name, err.Error()) + } + } + op, err = gce.service.TargetPools.Delete(gce.projectID, region, name).Do() + if err != nil { + glog.Warningln("Failed to delete Target Pool %s, got error %s.", name, err.Error()) + return err + } + err = gce.waitForRegionOp(op, region) + if err != nil { + glog.Warningln("Failed waiting for Target Pool %s to be deleted: got error %s.", name, err.Error()) } - _, err = gce.service.TargetPools.Delete(gce.projectID, region, name).Do() return err } @@ -279,19 +304,37 @@ func canonicalizeInstanceName(name string) string { return name } -// IPAddress is an implementation of Instances.IPAddress. -func (gce *GCECloud) IPAddress(instance string) (net.IP, error) { - instance = canonicalizeInstanceName(instance) - res, err := gce.service.Instances.Get(gce.projectID, gce.zone, instance).Do() +// Return the instances matching the relevant name. +func (gce *GCECloud) getInstanceByName(name string) (*compute.Instance, error) { + name = canonicalizeInstanceName(name) + res, err := gce.service.Instances.Get(gce.projectID, gce.zone, name).Do() + if err != nil { + glog.Errorf("Failed to retrieve TargetInstance resource for instance:%s", name) + return nil, err + } + return res, nil +} + +// NodeAddresses is an implementation of Instances.NodeAddresses. +func (gce *GCECloud) NodeAddresses(instance string) ([]api.NodeAddress, error) { + inst, err := gce.getInstanceByName(instance) if err != nil { - glog.Errorf("Failed to retrieve TargetInstance resource for instance:%s", instance) return nil, err } - ip := net.ParseIP(res.NetworkInterfaces[0].AccessConfigs[0].NatIP) + ip := net.ParseIP(inst.NetworkInterfaces[0].AccessConfigs[0].NatIP) if ip == nil { - return nil, fmt.Errorf("invalid network IP: %s", res.NetworkInterfaces[0].AccessConfigs[0].NatIP) + return nil, fmt.Errorf("invalid network IP: %s", inst.NetworkInterfaces[0].AccessConfigs[0].NatIP) + } + return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: ip.String()}}, nil +} + +// ExternalID returns the cloud provider ID of the specified instance. +func (gce *GCECloud) ExternalID(instance string) (string, error) { + inst, err := gce.getInstanceByName(instance) + if err != nil { + return "", err } - return ip, nil + return strconv.FormatUint(inst.Id, 10), nil } // fqdnSuffix is hacky function to compute the delta between hostame and hostname -f. diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack/openstack.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack/openstack.go index 4e5947d6cca3..2d07106cc8db 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack/openstack.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack/openstack.go @@ -46,6 +46,11 @@ var ErrMultipleResults = errors.New("Multiple results where only one expected") var ErrNoAddressFound = errors.New("No address found for host") var ErrAttrNotFound = errors.New("Expected attribute not found") +const ( + MiB = 1024 * 1024 + GB = 1000 * 1000 * 1000 +) + // encoding.TextUnmarshaler interface for time.Duration type MyDuration struct { time.Duration @@ -71,6 +76,7 @@ type LoadBalancerOpts struct { // OpenStack is an implementation of cloud provider Interface for OpenStack. type OpenStack struct { provider *gophercloud.ProviderClient + authOpts gophercloud.AuthOptions region string lbOpts LoadBalancerOpts } @@ -111,7 +117,11 @@ func (cfg Config) toAuthOptions() gophercloud.AuthOptions { TenantID: cfg.Global.TenantId, TenantName: cfg.Global.TenantName, - // Persistent service, so we need to be able to renew tokens + // Persistent service, so we need to be able to renew + // tokens. + // (gophercloud doesn't appear to actually reauth yet, + // hence the explicit openstack.Authenticate() calls + // below) AllowReauth: true, } } @@ -128,13 +138,15 @@ func readConfig(config io.Reader) (Config, error) { } func newOpenStack(cfg Config) (*OpenStack, error) { - provider, err := openstack.AuthenticatedClient(cfg.toAuthOptions()) + authOpts := cfg.toAuthOptions() + provider, err := openstack.AuthenticatedClient(authOpts) if err != nil { return nil, err } os := OpenStack{ provider: provider, + authOpts: authOpts, region: cfg.Global.Region, lbOpts: cfg.LoadBalancer, } @@ -148,7 +160,12 @@ type Instances struct { // Instances returns an implementation of Instances for OpenStack. func (os *OpenStack) Instances() (cloudprovider.Instances, bool) { - glog.V(2).Info("openstack.Instances() called") + glog.V(4).Info("openstack.Instances() called") + + if err := openstack.Authenticate(os.provider, os.authOpts); err != nil { + glog.Warningf("Failed to reauthenticate: %v", err) + return nil, false + } compute, err := openstack.NewComputeV2(os.provider, gophercloud.EndpointOpts{ Region: os.region, @@ -169,11 +186,11 @@ func (os *OpenStack) Instances() (cloudprovider.Instances, bool) { for _, flavor := range flavorList { rsrc := api.NodeResources{ Capacity: api.ResourceList{ - api.ResourceCPU: *resource.NewMilliQuantity(int64(flavor.VCPUs*1000), resource.DecimalSI), - api.ResourceMemory: resource.MustParse(fmt.Sprintf("%dMi", flavor.RAM)), - "openstack.org/disk": resource.MustParse(fmt.Sprintf("%dG", flavor.Disk)), - "openstack.org/rxTxFactor": *resource.NewQuantity(int64(flavor.RxTxFactor*1000), resource.DecimalSI), - "openstack.org/swap": resource.MustParse(fmt.Sprintf("%dMi", flavor.Swap)), + api.ResourceCPU: *resource.NewQuantity(int64(flavor.VCPUs), resource.DecimalSI), + api.ResourceMemory: *resource.NewQuantity(int64(flavor.RAM)*MiB, resource.BinarySI), + "openstack.org/disk": *resource.NewQuantity(int64(flavor.Disk)*GB, resource.DecimalSI), + "openstack.org/rxTxFactor": *resource.NewMilliQuantity(int64(flavor.RxTxFactor)*1000, resource.DecimalSI), + "openstack.org/swap": *resource.NewQuantity(int64(flavor.Swap)*MiB, resource.BinarySI), }, } flavor_to_resource[flavor.ID] = &rsrc @@ -185,14 +202,14 @@ func (os *OpenStack) Instances() (cloudprovider.Instances, bool) { return nil, false } - glog.V(2).Infof("Found %v compute flavors", len(flavor_to_resource)) + glog.V(3).Infof("Found %v compute flavors", len(flavor_to_resource)) glog.V(1).Info("Claiming to support Instances") return &Instances{compute, flavor_to_resource}, true } func (i *Instances) List(name_filter string) ([]string, error) { - glog.V(2).Infof("openstack List(%v) called", name_filter) + glog.V(4).Infof("openstack List(%v) called", name_filter) opts := servers.ListOpts{ Name: name_filter, @@ -215,7 +232,8 @@ func (i *Instances) List(name_filter string) ([]string, error) { return nil, err } - glog.V(2).Infof("Found %v entries: %v", len(ret), ret) + glog.V(3).Infof("Found %v instances matching %v: %v", + len(ret), name_filter, ret) return ret, nil } @@ -253,25 +271,29 @@ func getServerByName(client *gophercloud.ServiceClient, name string) (*servers.S return &serverList[0], nil } -func firstAddr(netblob interface{}) string { +func findAddrs(netblob interface{}) []string { // Run-time types for the win :( + ret := []string{} list, ok := netblob.([]interface{}) - if !ok || len(list) < 1 { - return "" - } - props, ok := list[0].(map[string]interface{}) - if !ok { - return "" - } - tmp, ok := props["addr"] if !ok { - return "" + return ret } - addr, ok := tmp.(string) - if !ok { - return "" + for _, item := range list { + props, ok := item.(map[string]interface{}) + if !ok { + continue + } + tmp, ok := props["addr"] + if !ok { + continue + } + addr, ok := tmp.(string) + if !ok { + continue + } + ret = append(ret, addr) } - return addr + return ret } func getAddressByName(api *gophercloud.ServiceClient, name string) (string, error) { @@ -282,10 +304,14 @@ func getAddressByName(api *gophercloud.ServiceClient, name string) (string, erro var s string if s == "" { - s = firstAddr(srv.Addresses["private"]) + if tmp := findAddrs(srv.Addresses["private"]); len(tmp) >= 1 { + s = tmp[0] + } } if s == "" { - s = firstAddr(srv.Addresses["public"]) + if tmp := findAddrs(srv.Addresses["public"]); len(tmp) >= 1 { + s = tmp[0] + } } if s == "" { s = srv.AccessIPv4 @@ -299,21 +325,57 @@ func getAddressByName(api *gophercloud.ServiceClient, name string) (string, erro return s, nil } -func (i *Instances) IPAddress(name string) (net.IP, error) { - glog.V(2).Infof("IPAddress(%v) called", name) +func (i *Instances) NodeAddresses(name string) ([]api.NodeAddress, error) { + glog.V(4).Infof("NodeAddresses(%v) called", name) - ip, err := getAddressByName(i.compute, name) + srv, err := getServerByName(i.compute, name) if err != nil { return nil, err } - glog.V(2).Infof("IPAddress(%v) => %v", name, ip) + addrs := []api.NodeAddress{} + + for _, addr := range findAddrs(srv.Addresses["private"]) { + addrs = append(addrs, api.NodeAddress{ + Type: api.NodeInternalIP, + Address: addr, + }) + } + + for _, addr := range findAddrs(srv.Addresses["public"]) { + addrs = append(addrs, api.NodeAddress{ + Type: api.NodeExternalIP, + Address: addr, + }) + } + + // AccessIPs are usually duplicates of "public" addresses. + api.AddToNodeAddresses(&addrs, + api.NodeAddress{ + Type: api.NodeExternalIP, + Address: srv.AccessIPv6, + }, + api.NodeAddress{ + Type: api.NodeExternalIP, + Address: srv.AccessIPv4, + }, + ) - return net.ParseIP(ip), err + glog.V(4).Infof("NodeAddresses(%v) => %v", name, addrs) + return addrs, nil +} + +// ExternalID returns the cloud provider ID of the specified instance. +func (i *Instances) ExternalID(name string) (string, error) { + srv, err := getServerByName(i.compute, name) + if err != nil { + return "", err + } + return srv.ID, nil } func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { - glog.V(2).Infof("GetNodeResources(%v) called", name) + glog.V(4).Infof("GetNodeResources(%v) called", name) srv, err := getServerByName(i.compute, name) if err != nil { @@ -333,7 +395,7 @@ func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { return nil, ErrNotFound } - glog.V(2).Infof("GetNodeResources(%v) => %v", name, rsrc) + glog.V(4).Infof("GetNodeResources(%v) => %v", name, rsrc) return rsrc, nil } @@ -349,6 +411,13 @@ type LoadBalancer struct { } func (os *OpenStack) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { + glog.V(4).Info("openstack.TCPLoadBalancer() called") + + if err := openstack.Authenticate(os.provider, os.authOpts); err != nil { + glog.Warningf("Failed to reauthenticate: %v", err) + return nil, false + } + // TODO: Search for and support Rackspace loadbalancer API, and others. network, err := openstack.NewNetworkV2(os.provider, gophercloud.EndpointOpts{ Region: os.region, @@ -416,24 +485,32 @@ func (lb *LoadBalancer) TCPLoadBalancerExists(name, region string) (bool, error) // a list of regions (from config) and query/create loadbalancers in // each region. -func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinity api.AffinityType) (net.IP, error) { - glog.V(2).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v)", name, region, externalIP, port, hosts) - if affinity != api.AffinityTypeNone { - return nil, fmt.Errorf("unsupported load balancer affinity: %v", affinity) +func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinity api.AffinityType) (string, error) { + glog.V(4).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, externalIP, port, hosts, affinity) + + var persistence *vips.SessionPersistence + switch affinity { + case api.AffinityTypeNone: + persistence = nil + case api.AffinityTypeClientIP: + persistence = &vips.SessionPersistence{Type: "SOURCE_IP"} + default: + return "", fmt.Errorf("unsupported load balancer affinity: %v", affinity) } + pool, err := pools.Create(lb.network, pools.CreateOpts{ Name: name, Protocol: pools.ProtocolTCP, SubnetID: lb.opts.SubnetId, }).Extract() if err != nil { - return nil, err + return "", err } for _, host := range hosts { addr, err := getAddressByName(lb.compute, host) if err != nil { - return nil, err + return "", err } _, err = members.Create(lb.network, members.CreateOpts{ @@ -443,7 +520,7 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne }).Extract() if err != nil { pools.Delete(lb.network, pool.ID) - return nil, err + return "", err } } @@ -457,14 +534,14 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne }).Extract() if err != nil { pools.Delete(lb.network, pool.ID) - return nil, err + return "", err } _, err = pools.AssociateMonitor(lb.network, pool.ID, mon.ID).Extract() if err != nil { monitors.Delete(lb.network, mon.ID) pools.Delete(lb.network, pool.ID) - return nil, err + return "", err } } @@ -475,20 +552,21 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne Protocol: "TCP", ProtocolPort: port, PoolID: pool.ID, + Persistence: persistence, }).Extract() if err != nil { if mon != nil { monitors.Delete(lb.network, mon.ID) } pools.Delete(lb.network, pool.ID) - return nil, err + return "", err } - return net.ParseIP(vip.Address), nil + return vip.Address, nil } func (lb *LoadBalancer) UpdateTCPLoadBalancer(name, region string, hosts []string) error { - glog.V(2).Infof("UpdateTCPLoadBalancer(%v, %v, %v)", name, region, hosts) + glog.V(4).Infof("UpdateTCPLoadBalancer(%v, %v, %v)", name, region, hosts) vip, err := getVipByName(lb.network, name) if err != nil { @@ -549,7 +627,7 @@ func (lb *LoadBalancer) UpdateTCPLoadBalancer(name, region string, hosts []strin } func (lb *LoadBalancer) DeleteTCPLoadBalancer(name, region string) error { - glog.V(2).Infof("DeleteTCPLoadBalancer(%v, %v)", name, region) + glog.V(4).Infof("DeleteTCPLoadBalancer(%v, %v)", name, region) vip, err := getVipByName(lb.network, name) if err != nil { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack/openstack_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack/openstack_test.go index 463d7e20d471..8b3c0850841d 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack/openstack_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack/openstack_test.go @@ -144,11 +144,11 @@ func TestInstances(t *testing.T) { } t.Logf("Found servers (%d): %s\n", len(srvs), srvs) - ip, err := i.IPAddress(srvs[0]) + addrs, err := i.NodeAddresses(srvs[0]) if err != nil { - t.Fatalf("Instances.IPAddress(%s) failed: %s", srvs[0], err) + t.Fatalf("Instances.NodeAddresses(%s) failed: %s", srvs[0], err) } - t.Logf("Found IPAddress(%s) = %s\n", srvs[0], ip) + t.Logf("Found NodeAddresses(%s) = %s\n", srvs[0], addrs) rsrcs, err := i.GetNodeResources(srvs[0]) if err != nil { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt/ovirt.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt/ovirt.go index 187da1eddfe3..f6601017d2f3 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt/ovirt.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt/ovirt.go @@ -25,6 +25,7 @@ import ( "net/http" "net/url" "path" + "sort" "strings" "code.google.com/p/gcfg" @@ -32,6 +33,14 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" ) +type OVirtInstance struct { + UUID string + Name string + IPAddress string +} + +type OVirtInstanceMap map[string]OVirtInstance + type OVirtCloud struct { VmsRequest *url.URL HostsRequest *url.URL @@ -48,9 +57,16 @@ type OVirtApiConfig struct { } } +type XmlVmAddress struct { + Address string `xml:"address,attr"` +} + type XmlVmInfo struct { - Hostname string `xml:"guest_info>fqdn"` - State string `xml:"status>state"` + UUID string `xml:"id,attr"` + Name string `xml:"name"` + Hostname string `xml:"guest_info>fqdn"` + Addresses []XmlVmAddress `xml:"guest_info>ips>ip"` + State string `xml:"status>state"` } type XmlVmsList struct { @@ -114,17 +130,41 @@ func (v *OVirtCloud) Zones() (cloudprovider.Zones, bool) { return nil, false } -// IPAddress returns the address of a particular machine instance -func (v *OVirtCloud) IPAddress(instance string) (net.IP, error) { - // since the instance now is the IP in the ovirt env, this is trivial no-op - ip, err := net.LookupIP(instance) - if err != nil || len(ip) < 1 { - return nil, fmt.Errorf("cannot find ip address for: %s", instance) +// NodeAddresses returns the NodeAddresses of a particular machine instance +func (v *OVirtCloud) NodeAddresses(name string) ([]api.NodeAddress, error) { + instance, err := v.fetchInstance(name) + if err != nil { + return nil, err + } + + var address net.IP + + if instance.IPAddress != "" { + address = net.ParseIP(instance.IPAddress) + if address == nil { + return nil, fmt.Errorf("couldn't parse address: %s", instance.IPAddress) + } + } else { + resolved, err := net.LookupIP(name) + if err != nil || len(resolved) < 1 { + return nil, fmt.Errorf("couldn't lookup address: %s", name) + } + address = resolved[0] + } + + return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: address.String()}}, nil +} + +// ExternalID returns the cloud provider ID of the specified instance. +func (v *OVirtCloud) ExternalID(name string) (string, error) { + instance, err := v.fetchInstance(name) + if err != nil { + return "", err } - return ip[0], nil + return instance.UUID, nil } -func getInstancesFromXml(body io.Reader) ([]string, error) { +func getInstancesFromXml(body io.Reader) (OVirtInstanceMap, error) { if body == nil { return nil, fmt.Errorf("ovirt rest-api response body is missing") } @@ -140,20 +180,28 @@ func getInstancesFromXml(body io.Reader) ([]string, error) { return nil, err } - var instances []string + instances := make(OVirtInstanceMap) for _, vm := range vmlist.Vm { // Always return only vms that are up and running if vm.Hostname != "" && strings.ToLower(vm.State) == "up" { - instances = append(instances, vm.Hostname) + address := "" + if len(vm.Addresses) > 0 { + address = vm.Addresses[0].Address + } + + instances[vm.Hostname] = OVirtInstance{ + UUID: vm.UUID, + Name: vm.Name, + IPAddress: address, + } } } return instances, nil } -// List enumerates the set of minions instances known by the cloud provider -func (v *OVirtCloud) List(filter string) ([]string, error) { +func (v *OVirtCloud) fetchAllInstances() (OVirtInstanceMap, error) { response, err := http.Get(v.VmsRequest.String()) if err != nil { return nil, err @@ -164,6 +212,41 @@ func (v *OVirtCloud) List(filter string) ([]string, error) { return getInstancesFromXml(response.Body) } +func (v *OVirtCloud) fetchInstance(name string) (*OVirtInstance, error) { + allInstances, err := v.fetchAllInstances() + if err != nil { + return nil, err + } + + instance, found := allInstances[name] + if !found { + return nil, fmt.Errorf("cannot find instance: %s", name) + } + + return &instance, nil +} + +func (m *OVirtInstanceMap) ListSortedNames() []string { + var names []string + + for k := range *m { + names = append(names, k) + } + + sort.Strings(names) + + return names +} + +// List enumerates the set of minions instances known by the cloud provider +func (v *OVirtCloud) List(filter string) ([]string, error) { + instances, err := v.fetchAllInstances() + if err != nil { + return nil, err + } + return instances.ListSortedNames(), nil +} + func (v *OVirtCloud) GetNodeResources(name string) (*api.NodeResources, error) { return nil, nil } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt/ovirt_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt/ovirt_test.go index 6f79cf01703e..afeb45c4a9ab 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt/ovirt_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt/ovirt_test.go @@ -118,7 +118,9 @@ func TestOVirtCloudXmlParsing(t *testing.T) { if len(instances4) != 2 { t.Fatalf("Unexpected number of instance(s): %d", len(instances4)) } - if instances4[0] != "host1" || instances4[1] != "host3" { + + names := instances4.ListSortedNames() + if names[0] != "host1" || names[1] != "host3" { t.Fatalf("Unexpected instance(s): %s", instances4) } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace/rackspace.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace/rackspace.go index 5ab564c9ee24..3871776e1d66 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace/rackspace.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace/rackspace.go @@ -350,17 +350,23 @@ func getAddressByName(api *gophercloud.ServiceClient, name string) (string, erro return s, nil } -func (i *Instances) IPAddress(name string) (net.IP, error) { - glog.V(2).Infof("IPAddress(%v) called", name) +func (i *Instances) NodeAddresses(name string) ([]api.NodeAddress, error) { + glog.V(2).Infof("NodeAddresses(%v) called", name) ip, err := getAddressByName(i.compute, name) if err != nil { return nil, err } - glog.V(2).Infof("IPAddress(%v) => %v", name, ip) + glog.V(2).Infof("NodeAddresses(%v) => %v", name, ip) - return net.ParseIP(ip), err + // net.ParseIP().String() is to maintain compatibility with the old code + return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: net.ParseIP(ip).String()}}, nil +} + +// ExternalID returns the cloud provider ID of the specified instance. +func (i *Instances) ExternalID(name string) (string, error) { + return "", fmt.Errorf("unimplemented") } func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace/rackspace_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace/rackspace_test.go index f72d7cfb4496..48b1ec4bfe72 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace/rackspace_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/rackspace/rackspace_test.go @@ -144,11 +144,11 @@ func TestInstances(t *testing.T) { } t.Logf("Found servers (%d): %s\n", len(srvs), srvs) - ip, err := i.IPAddress(srvs[0]) + addrs, err := i.NodeAddresses(srvs[0]) if err != nil { - t.Fatalf("Instances.IPAddress(%s) failed: %s", srvs[0], err) + t.Fatalf("Instances.NodeAddresses(%s) failed: %s", srvs[0], err) } - t.Logf("Found IPAddress(%s) = %s\n", srvs[0], ip) + t.Logf("Found NodeAddresses(%s) = %s\n", srvs[0], addrs) rsrcs, err := i.GetNodeResources(srvs[0]) if err != nil { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant/vagrant.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant/vagrant.go index 3a7c6b4c1796..9672ba43b49f 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant/vagrant.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant/vagrant.go @@ -99,8 +99,8 @@ func (v *VagrantCloud) Zones() (cloudprovider.Zones, bool) { return nil, false } -// IPAddress returns the address of a particular machine instance. -func (v *VagrantCloud) IPAddress(instance string) (net.IP, error) { +// getInstanceByAddress retuns +func (v *VagrantCloud) getInstanceByAddress(address string) (*SaltMinion, error) { token, err := v.saltLogin() if err != nil { return nil, err @@ -112,11 +112,32 @@ func (v *VagrantCloud) IPAddress(instance string) (net.IP, error) { filteredMinions := v.saltMinionsByRole(minions, "kubernetes-pool") for _, minion := range filteredMinions { // Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time - if minion.IP == instance { - return net.ParseIP(minion.IP), nil + if minion.IP == address { + return &minion, nil } } - return nil, fmt.Errorf("unable to find IP address for instance: %s", instance) + return nil, fmt.Errorf("unable to find instance for address: %s", address) +} + +// NodeAddresses returns the NodeAddresses of a particular machine instance. +func (v *VagrantCloud) NodeAddresses(instance string) ([]api.NodeAddress, error) { + // Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time + minion, err := v.getInstanceByAddress(instance) + if err != nil { + return nil, err + } + ip := net.ParseIP(minion.IP) + return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: ip.String()}}, nil +} + +// ExternalID returns the cloud provider ID of the specified instance. +func (v *VagrantCloud) ExternalID(instance string) (string, error) { + // Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time + minion, err := v.getInstanceByAddress(instance) + if err != nil { + return "", err + } + return minion.IP, nil } // saltMinionsByRole filters a list of minions that have a matching role. diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant/vagrant_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant/vagrant_test.go index 3ed9f0037c21..8012f7db09ab 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant/vagrant_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant/vagrant_test.go @@ -81,12 +81,14 @@ func TestVagrantCloud(t *testing.T) { t.Fatalf("Invalid instance returned") } - ip, err := vagrantCloud.IPAddress(instances[0]) + addrs, err := vagrantCloud.NodeAddresses(instances[0]) if err != nil { - t.Fatalf("Unexpected error, should have returned a valid IP address: %s", err) + t.Fatalf("Unexpected error, should have returned valid NodeAddresses: %s", err) } - - if ip.String() != expectedInstanceIP { + if len(addrs) != 1 { + t.Fatalf("should have returned exactly one NodeAddress: %v", addrs) + } + if addrs[0].Address != expectedInstanceIP { t.Fatalf("Invalid IP address returned") } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/constraint/constraint_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/constraint/constraint_test.go deleted file mode 100644 index eae452212701..000000000000 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/constraint/constraint_test.go +++ /dev/null @@ -1,99 +0,0 @@ -/* -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 constraint - -import ( - "fmt" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -func containerWithHostPorts(ports ...int) api.Container { - c := api.Container{} - for _, p := range ports { - c.Ports = append(c.Ports, api.Port{HostPort: p}) - } - return c -} - -func podWithContainers(containers ...api.Container) api.BoundPod { - m := api.BoundPod{} - for _, c := range containers { - m.Spec.Containers = append(m.Spec.Containers, c) - } - return m -} - -func TestAllowed(t *testing.T) { - table := []struct { - err string - pods []api.BoundPod - }{ - { - err: "[]", - pods: []api.BoundPod{ - podWithContainers( - containerWithHostPorts(1, 2, 3), - containerWithHostPorts(4, 5, 6), - ), - podWithContainers( - containerWithHostPorts(7, 8, 9), - containerWithHostPorts(10, 11, 12), - ), - }, - }, - { - err: "[]", - pods: []api.BoundPod{ - podWithContainers( - containerWithHostPorts(0, 0), - containerWithHostPorts(0, 0), - ), - podWithContainers( - containerWithHostPorts(0, 0), - containerWithHostPorts(0, 0), - ), - }, - }, - { - err: "[host port 3 is already in use]", - pods: []api.BoundPod{ - podWithContainers( - containerWithHostPorts(3, 3), - ), - }, - }, - { - err: "[host port 6 is already in use]", - pods: []api.BoundPod{ - podWithContainers( - containerWithHostPorts(6), - ), - podWithContainers( - containerWithHostPorts(6), - ), - }, - }, - } - - for _, item := range table { - if e, a := item.err, Allowed(item.pods); e != fmt.Sprintf("%v", a) { - t.Errorf("Expected %v, got %v: \n%v\v", e, a, item.pods) - } - } -} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/constraint/ports.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/constraint/ports.go deleted file mode 100644 index b7398a6ea78d..000000000000 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/constraint/ports.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -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 constraint - -import ( - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -// hostPortsConflict returns an array of host ports that at least two -// containers attempt to expose. The array is empty if no such port -// exists. -func hostPortsConflict(pods []api.BoundPod) []int { - hostPorts := map[int]struct{}{} - conflictingPorts := []int{} - for _, pod := range pods { - for _, container := range pod.Spec.Containers { - for _, port := range container.Ports { - if port.HostPort == 0 { - continue - } - if _, exists := hostPorts[port.HostPort]; exists { - conflictingPorts = append(conflictingPorts, port.HostPort) - } - hostPorts[port.HostPort] = struct{}{} - } - } - } - return conflictingPorts -} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/controller/replication_controller.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/controller/replication_controller.go index ba9353c585f2..c815602b5f5a 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/controller/replication_controller.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/controller/replication_controller.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" @@ -56,6 +57,9 @@ type RealPodControl struct { kubeClient client.Interface } +// Time period of main replication controller sync loop +const DefaultSyncPeriod = 10 * time.Second + func (r RealPodControl) createReplica(namespace string, controller api.ReplicationController) { desiredLabels := make(labels.Set) for k, v := range controller.Spec.Template.Labels { @@ -119,7 +123,7 @@ func (rm *ReplicationManager) Run(period time.Duration) { func (rm *ReplicationManager) watchControllers(resourceVersion *string) { watching, err := rm.kubeClient.ReplicationControllers(api.NamespaceAll).Watch( labels.Everything(), - labels.Everything(), + fields.Everything(), *resourceVersion, ) if err != nil { @@ -135,7 +139,7 @@ func (rm *ReplicationManager) watchControllers(resourceVersion *string) { case event, open := <-watching.ResultChan(): if !open { // watchChannel has been closed, or something else went - // wrong with our etcd watch call. Let the util.Forever() + // wrong with our watch call. Let the util.Forever() // that called us call us again. return } @@ -193,7 +197,8 @@ func (rm *ReplicationManager) syncReplicationController(controller api.Replicati return err } filteredList := FilterActivePods(podList.Items) - diff := len(filteredList) - controller.Spec.Replicas + activePods := len(filteredList) + diff := activePods - controller.Spec.Replicas if diff < 0 { diff *= -1 wait := sync.WaitGroup{} @@ -218,6 +223,13 @@ func (rm *ReplicationManager) syncReplicationController(controller api.Replicati } wait.Wait() } + if controller.Status.Replicas != activePods { + controller.Status.Replicas = activePods + _, err = rm.kubeClient.ReplicationControllers(controller.Namespace).Update(&controller) + if err != nil { + return err + } + } return nil } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/controller/replication_controller_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/controller/replication_controller_test.go index bf9b772bb7da..735ef1dc16d4 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/controller/replication_controller_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/controller/replication_controller_test.go @@ -30,10 +30,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" - "github.com/coreos/go-etcd/etcd" ) func makeNamespaceURL(namespace, suffix string) string { @@ -68,6 +66,8 @@ func (f *FakePodControl) deletePod(namespace string, podName string) error { func newReplicationController(replicas int) api.ReplicationController { return api.ReplicationController{ + TypeMeta: api.TypeMeta{APIVersion: testapi.Version()}, + ObjectMeta: api.ObjectMeta{Name: "foobar", Namespace: "default", ResourceVersion: "18"}, Spec: api.ReplicationControllerSpec{ Replicas: replicas, Template: &api.PodTemplateSpec{ @@ -81,8 +81,12 @@ func newReplicationController(replicas int) api.ReplicationController { Containers: []api.Container{ { Image: "foo/bar", + TerminationMessagePath: api.TerminationMessagePathDefault, + ImagePullPolicy: api.PullIfNotPresent, }, }, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSDefault, NodeSelector: map[string]string{ "baz": "blah", }, @@ -159,23 +163,37 @@ func TestSyncReplicationControllerDeletes(t *testing.T) { func TestSyncReplicationControllerCreates(t *testing.T) { body := runtime.EncodeOrDie(testapi.Codec(), newPodList(0)) - fakeHandler := util.FakeHandler{ + fakePodHandler := util.FakeHandler{ StatusCode: 200, ResponseBody: string(body), } - testServer := httptest.NewServer(&fakeHandler) + fakePodControl := FakePodControl{} + + controller := newReplicationController(2) + fakeUpdateHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: runtime.EncodeOrDie(testapi.Codec(), &controller), + T: t, + } + + testServerMux := http.NewServeMux() + testServerMux.Handle("/api/"+testapi.Version()+"/pods/", &fakePodHandler) + testServerMux.Handle(fmt.Sprintf("/api/"+testapi.Version()+"/replicationControllers/%s", controller.Name), &fakeUpdateHandler) + testServer := httptest.NewServer(testServerMux) defer testServer.Close() client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) - fakePodControl := FakePodControl{} - manager := NewReplicationManager(client) manager.podControl = &fakePodControl - - controllerSpec := newReplicationController(2) - - manager.syncReplicationController(controllerSpec) + manager.syncReplicationController(controller) validateSyncReplication(t, &fakePodControl, 2, 0) + + // No Status.Replicas update expected even though 2 pods were just created, + // because the controller manager can't observe the pods till the next sync cycle. + if fakeUpdateHandler.RequestReceived != nil { + t.Errorf("Unexpected updates for controller via %v", + fakeUpdateHandler.RequestReceived.URL) + } } func TestCreateReplica(t *testing.T) { @@ -193,33 +211,7 @@ func TestCreateReplica(t *testing.T) { kubeClient: client, } - controllerSpec := api.ReplicationController{ - ObjectMeta: api.ObjectMeta{ - Name: "test", - }, - Spec: api.ReplicationControllerSpec{ - Template: &api.PodTemplateSpec{ - ObjectMeta: api.ObjectMeta{ - Labels: map[string]string{ - "name": "foo", - "type": "production", - "replicationController": "test", - }, - }, - Spec: api.PodSpec{ - Containers: []api.Container{ - { - Image: "foo/bar", - }, - }, - NodeSelector: map[string]string{ - "foo": "bar", - }, - }, - }, - }, - } - + controllerSpec := newReplicationController(1) podControl.createReplica(ns, controllerSpec) manifest := api.ContainerManifest{} @@ -245,64 +237,13 @@ func TestCreateReplica(t *testing.T) { } } -func TestSynchonize(t *testing.T) { - controllerSpec1 := api.ReplicationController{ - TypeMeta: api.TypeMeta{APIVersion: testapi.Version()}, - Spec: api.ReplicationControllerSpec{ - Replicas: 4, - Template: &api.PodTemplateSpec{ - ObjectMeta: api.ObjectMeta{ - Labels: map[string]string{ - "name": "foo", - "type": "production", - }, - }, - Spec: api.PodSpec{ - Containers: []api.Container{ - { - Image: "foo/bar", - }, - }, - }, - }, - }, - } - controllerSpec2 := api.ReplicationController{ - TypeMeta: api.TypeMeta{APIVersion: testapi.Version()}, - Spec: api.ReplicationControllerSpec{ - Replicas: 3, - Template: &api.PodTemplateSpec{ - ObjectMeta: api.ObjectMeta{ - Labels: map[string]string{ - "name": "bar", - "type": "production", - }, - }, - Spec: api.PodSpec{ - Containers: []api.Container{ - { - Image: "bar/baz", - }, - }, - }, - }, - }, - } - - fakeEtcd := tools.NewFakeEtcdClient(t) - fakeEtcd.Data["/registry/controllers"] = tools.EtcdResponseWithError{ - R: &etcd.Response{ - Node: &etcd.Node{ - Nodes: []*etcd.Node{ - { - Value: runtime.EncodeOrDie(testapi.Codec(), &controllerSpec1), - }, - { - Value: runtime.EncodeOrDie(testapi.Codec(), &controllerSpec2), - }, - }, - }, - }, +func TestSynchronize(t *testing.T) { + controllerSpec1 := newReplicationController(4) + controllerSpec2 := newReplicationController(3) + controllerSpec2.Name = "bar" + controllerSpec2.Spec.Template.ObjectMeta.Labels = map[string]string{ + "name": "bar", + "type": "production", } fakePodHandler := util.FakeHandler{ @@ -339,6 +280,106 @@ func TestSynchonize(t *testing.T) { validateSyncReplication(t, &fakePodControl, 7, 0) } +func TestControllerNoReplicaUpdate(t *testing.T) { + // Steady state for the replication controller, no Status.Replicas updates expected + rc := newReplicationController(5) + rc.Status = api.ReplicationControllerStatus{Replicas: 5} + activePods := 5 + + body, _ := latest.Codec.Encode(newPodList(activePods)) + fakePodHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: string(body), + T: t, + } + fakeControllerHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: runtime.EncodeOrDie(latest.Codec, &api.ReplicationControllerList{ + Items: []api.ReplicationController{rc}, + }), + T: t, + } + fakeUpdateHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: runtime.EncodeOrDie(testapi.Codec(), &rc), + T: t, + } + + mux := http.NewServeMux() + mux.Handle("/api/"+testapi.Version()+"/pods/", &fakePodHandler) + mux.Handle("/api/"+testapi.Version()+"/replicationControllers/", &fakeControllerHandler) + mux.Handle(fmt.Sprintf("/api/"+testapi.Version()+"/replicationControllers/%s", rc.Name), &fakeUpdateHandler) + mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusNotFound) + t.Errorf("Unexpected request for %v", req.RequestURI) + }) + testServer := httptest.NewServer(mux) + defer testServer.Close() + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) + manager := NewReplicationManager(client) + fakePodControl := FakePodControl{} + manager.podControl = &fakePodControl + + manager.synchronize() + + validateSyncReplication(t, &fakePodControl, 0, 0) + if fakeUpdateHandler.RequestReceived != nil { + t.Errorf("Unexpected updates for controller via %v", + fakeUpdateHandler.RequestReceived.URL) + } +} + +func TestControllerUpdateReplicas(t *testing.T) { + // Insufficient number of pods in the system, and Status.Replicas is wrong; + // Status.Replica should update to match number of pods in system, 1 new pod should be created. + rc := newReplicationController(5) + rc.Status = api.ReplicationControllerStatus{Replicas: 2} + activePods := 4 + + body, _ := latest.Codec.Encode(newPodList(activePods)) + fakePodHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: string(body), + T: t, + } + fakeControllerHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: runtime.EncodeOrDie(latest.Codec, &api.ReplicationControllerList{ + Items: []api.ReplicationController{rc}, + }), + T: t, + } + fakeUpdateHandler := util.FakeHandler{ + StatusCode: 200, + ResponseBody: runtime.EncodeOrDie(testapi.Codec(), &rc), + T: t, + } + + mux := http.NewServeMux() + + mux.Handle("/api/"+testapi.Version()+"/pods/", &fakePodHandler) + mux.Handle("/api/"+testapi.Version()+"/replicationControllers/", &fakeControllerHandler) + mux.Handle(fmt.Sprintf("/api/"+testapi.Version()+"/replicationControllers/%s", rc.Name), &fakeUpdateHandler) + mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusNotFound) + t.Errorf("Unexpected request for %v", req.RequestURI) + }) + testServer := httptest.NewServer(mux) + defer testServer.Close() + client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()}) + manager := NewReplicationManager(client) + fakePodControl := FakePodControl{} + manager.podControl = &fakePodControl + + manager.synchronize() + + // Status.Replicas should go up from 2->4 even though we created 5-4=1 pod + rc.Status = api.ReplicationControllerStatus{Replicas: 4} + decRc := runtime.EncodeOrDie(testapi.Codec(), &rc) + fakeUpdateHandler.ValidateRequest(t, fmt.Sprintf("/api/"+testapi.Version()+"/replicationControllers/%s?namespace=%s", rc.Name, rc.Namespace), "PUT", &decRc) + validateSyncReplication(t, &fakePodControl, 1, 0) +} + type FakeWatcher struct { w *watch.FakeWatcher *client.Fake diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/deep_equal.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/deep_equal.go index 2672cad67220..3ff089a5666a 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/deep_equal.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/deep_equal.go @@ -19,6 +19,7 @@ package conversion import ( "fmt" "reflect" + "strings" ) // Equalities is a map from type to a function comparing two values of @@ -99,10 +100,36 @@ type visit struct { typ reflect.Type } +// unexportedTypePanic is thrown when you use this DeepEqual on something that has an +// unexported type. It indicates a programmer error, so should not occur at runtime, +// which is why it's not public and thus impossible to catch. +type unexportedTypePanic []reflect.Type + +func (u unexportedTypePanic) Error() string { return u.String() } +func (u unexportedTypePanic) String() string { + strs := make([]string, len(u)) + for i, t := range u { + strs[i] = fmt.Sprintf("%v", t) + } + return "an unexported field was encountered, nested like this: " + strings.Join(strs, " -> ") +} + +func makeUsefulPanic(v reflect.Value) { + if x := recover(); x != nil { + if u, ok := x.(unexportedTypePanic); ok { + u = append(unexportedTypePanic{v.Type()}, u...) + x = u + } + panic(x) + } +} + // Tests for deep equality using reflected types. The map argument tracks // comparisons that have already been seen, which allows short circuiting on // recursive types. func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { + defer makeUsefulPanic(v1) + if !v1.IsValid() || !v2.IsValid() { return v1.IsValid() == v2.IsValid() } @@ -207,10 +234,10 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, return false default: // Normal equality suffices - if v1.CanInterface() && v2.CanInterface() { - return v1.Interface() == v2.Interface() + if !v1.CanInterface() || !v2.CanInterface() { + panic(unexportedTypePanic{}) } - return v1.CanInterface() == v2.CanInterface() + return v1.Interface() == v2.Interface() } } @@ -221,7 +248,8 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, // // An empty slice *is* equal to a nil slice for our purposes; same for maps. // -// Unexported field members are not compared. +// Unexported field members cannot be compared and will cause an imformative panic; you must add an Equality +// function for these types. func (e Equalities) DeepEqual(a1, a2 interface{}) bool { if a1 == nil || a2 == nil { return a1 == a2 @@ -235,6 +263,8 @@ func (e Equalities) DeepEqual(a1, a2 interface{}) bool { } func (e Equalities) deepValueDerive(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { + defer makeUsefulPanic(v1) + if !v1.IsValid() || !v2.IsValid() { return v1.IsValid() == v2.IsValid() } @@ -347,10 +377,10 @@ func (e Equalities) deepValueDerive(v1, v2 reflect.Value, visited map[visit]bool return false default: // Normal equality suffices - if v1.CanInterface() && v2.CanInterface() { - return v1.Interface() == v2.Interface() + if !v1.CanInterface() || !v2.CanInterface() { + panic(unexportedTypePanic{}) } - return v1.CanInterface() == v2.CanInterface() + return v1.Interface() == v2.Interface() } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/error.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/error.go index 7ee24ee43802..1c053c3eaae7 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/error.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/conversion/error.go @@ -49,3 +49,35 @@ func IsNotRegisteredError(err error) bool { _, ok := err.(*notRegisteredErr) return ok } + +type missingKindErr struct { + data string +} + +func (k *missingKindErr) Error() string { + return fmt.Sprintf("Object 'Kind' is missing in '%s'", k.data) +} + +func IsMissingKind(err error) bool { + if err == nil { + return false + } + _, ok := err.(*missingKindErr) + return ok +} + +type missingVersionErr struct { + data string +} + +func (k *missingVersionErr) Error() string { + return fmt.Sprintf("Object 'apiVersion' is missing in '%s'", k.data) +} + +func IsMissingVersion(err error) bool { + if err == nil { + return false + } + _, ok := err.(*missingVersionErr) + return ok +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider/gcp/jwt.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider/gcp/jwt.go new file mode 100644 index 000000000000..3cf7a7c71bf2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider/gcp/jwt.go @@ -0,0 +1,111 @@ +/* +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 gcp_credentials + +import ( + "io/ioutil" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider" + "github.com/golang/glog" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jwt" + + "github.com/spf13/pflag" +) + +const ( + storageReadOnlyScope = "https://www.googleapis.com/auth/devstorage.read_only" +) + +var ( + flagJwtFile = pflag.String("google_json_key", "", + "The Google Cloud Platform Service Account JSON Key to use for authentication.") +) + +// A DockerConfigProvider that reads its configuration from Google +// Compute Engine metadata. +type jwtProvider struct { + path *string + config *jwt.Config + tokenUrl string +} + +// init registers the various means by which credentials may +// be resolved on GCP. +func init() { + credentialprovider.RegisterCredentialProvider("google-jwt-key", + &credentialprovider.CachingDockerConfigProvider{ + Provider: &jwtProvider{ + path: flagJwtFile, + }, + Lifetime: 30 * time.Minute, + }) +} + +// Enabled implements DockerConfigProvider for the JSON Key based implementation. +func (j *jwtProvider) Enabled() bool { + if *j.path == "" { + return false + } + + data, err := ioutil.ReadFile(*j.path) + if err != nil { + glog.Errorf("while reading file %s got %v", *j.path, err) + return false + } + config, err := google.JWTConfigFromJSON(data, storageReadOnlyScope) + if err != nil { + glog.Errorf("while parsing %s data got %v", *j.path, err) + return false + } + + j.config = config + if j.tokenUrl != "" { + j.config.TokenURL = j.tokenUrl + } + return true +} + +// Provide implements DockerConfigProvider +func (j *jwtProvider) Provide() credentialprovider.DockerConfig { + cfg := credentialprovider.DockerConfig{} + + ts := j.config.TokenSource(oauth2.NoContext) + token, err := ts.Token() + if err != nil { + glog.Errorf("while exchanging json key %s for access token %v", *j.path, err) + return cfg + } + if !token.Valid() { + glog.Errorf("Got back invalid token: %v", token) + return cfg + } + + entry := credentialprovider.DockerConfigEntry{ + Username: "_token", + Password: token.AccessToken, + Email: j.config.Email, + } + + // Add our entry for each of the supported container registry URLs + for _, k := range containerRegistryUrls { + cfg[k] = entry + } + return cfg +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider/gcp/jwt_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider/gcp/jwt_test.go new file mode 100644 index 000000000000..fe44f9551fc0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider/gcp/jwt_test.go @@ -0,0 +1,121 @@ +/* +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 gcp_credentials + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider" +) + +const email = "foo@bar.com" + +// From oauth2/jwt_test.go +var ( + dummyPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAx4fm7dngEmOULNmAs1IGZ9Apfzh+BkaQ1dzkmbUgpcoghucE +DZRnAGd2aPyB6skGMXUytWQvNYav0WTR00wFtX1ohWTfv68HGXJ8QXCpyoSKSSFY +fuP9X36wBSkSX9J5DVgiuzD5VBdzUISSmapjKm+DcbRALjz6OUIPEWi1Tjl6p5RK +1w41qdbmt7E5/kGhKLDuT7+M83g4VWhgIvaAXtnhklDAggilPPa8ZJ1IFe31lNlr +k4DRk38nc6sEutdf3RL7QoH7FBusI7uXV03DC6dwN1kP4GE7bjJhcRb/7jYt7CQ9 +/E9Exz3c0yAp0yrTg0Fwh+qxfH9dKwN52S7SBwIDAQABAoIBAQCaCs26K07WY5Jt +3a2Cw3y2gPrIgTCqX6hJs7O5ByEhXZ8nBwsWANBUe4vrGaajQHdLj5OKfsIDrOvn +2NI1MqflqeAbu/kR32q3tq8/Rl+PPiwUsW3E6Pcf1orGMSNCXxeducF2iySySzh3 +nSIhCG5uwJDWI7a4+9KiieFgK1pt/Iv30q1SQS8IEntTfXYwANQrfKUVMmVF9aIK +6/WZE2yd5+q3wVVIJ6jsmTzoDCX6QQkkJICIYwCkglmVy5AeTckOVwcXL0jqw5Kf +5/soZJQwLEyBoQq7Kbpa26QHq+CJONetPP8Ssy8MJJXBT+u/bSseMb3Zsr5cr43e +DJOhwsThAoGBAPY6rPKl2NT/K7XfRCGm1sbWjUQyDShscwuWJ5+kD0yudnT/ZEJ1 +M3+KS/iOOAoHDdEDi9crRvMl0UfNa8MAcDKHflzxg2jg/QI+fTBjPP5GOX0lkZ9g +z6VePoVoQw2gpPFVNPPTxKfk27tEzbaffvOLGBEih0Kb7HTINkW8rIlzAoGBAM9y +1yr+jvfS1cGFtNU+Gotoihw2eMKtIqR03Yn3n0PK1nVCDKqwdUqCypz4+ml6cxRK +J8+Pfdh7D+ZJd4LEG6Y4QRDLuv5OA700tUoSHxMSNn3q9As4+T3MUyYxWKvTeu3U +f2NWP9ePU0lV8ttk7YlpVRaPQmc1qwooBA/z/8AdAoGAW9x0HWqmRICWTBnpjyxx +QGlW9rQ9mHEtUotIaRSJ6K/F3cxSGUEkX1a3FRnp6kPLcckC6NlqdNgNBd6rb2rA +cPl/uSkZP42Als+9YMoFPU/xrrDPbUhu72EDrj3Bllnyb168jKLa4VBOccUvggxr +Dm08I1hgYgdN5huzs7y6GeUCgYEAj+AZJSOJ6o1aXS6rfV3mMRve9bQ9yt8jcKXw +5HhOCEmMtaSKfnOF1Ziih34Sxsb7O2428DiX0mV/YHtBnPsAJidL0SdLWIapBzeg +KHArByIRkwE6IvJvwpGMdaex1PIGhx5i/3VZL9qiq/ElT05PhIb+UXgoWMabCp84 +OgxDK20CgYAeaFo8BdQ7FmVX2+EEejF+8xSge6WVLtkaon8bqcn6P0O8lLypoOhd +mJAYH8WU+UAy9pecUnDZj14LAGNVmYcse8HFX71MoshnvCTFEPVo4rZxIAGwMpeJ +5jgQ3slYLpqrGlcbLgUXBUgzEO684Wk/UV9DFPlHALVqCfXQ9dpJPg== +-----END RSA PRIVATE KEY-----` + + jsonKey = fmt.Sprintf(`{"private_key":"%[1]s", "client_email":"%[2]s"}`, + strings.Replace(dummyPrivateKey, "\n", "\\n", -1), email) +) + +func TestJwtProvider(t *testing.T) { + token := "asdhflkjsdfkjhsdf" + + // Modeled after oauth2/jwt_test.go + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(fmt.Sprintf(`{ + "access_token": "%[1]s", + "scope": "user", + "token_type": "bearer", + "expires_in": 3600 + }`, token))) + })) + defer ts.Close() + + file, err := ioutil.TempFile(os.TempDir(), "temp") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + + filename := file.Name() + _, err = file.WriteString(jsonKey) + if err != nil { + t.Fatalf("Error writing temp file: %v", err) + } + + provider := &jwtProvider{ + path: &filename, + tokenUrl: ts.URL, + } + if !provider.Enabled() { + t.Fatalf("Provider is unexpectedly disabled") + } + + keyring := &credentialprovider.BasicDockerKeyring{} + keyring.Add(provider.Provide()) + + // Verify that we get the expected username/password combo for + // a gcr.io image name. + registryUrl := "gcr.io/foo/bar" + val, ok := keyring.Lookup(registryUrl) + if !ok { + t.Errorf("Didn't find expected URL: %s", registryUrl) + } + + if "_token" != val.Username { + t.Errorf("Unexpected username value, want: _token, got: %s", val.Username) + } + if token != val.Password { + t.Errorf("Unexpected password value, want: %s, got: %s", token, val.Password) + } + if email != val.Email { + t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email) + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage/doc.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/doc.go similarity index 71% rename from Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage/doc.go rename to Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/doc.go index 9892fc3cba1e..2e5b8bc50e30 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage/doc.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2014 Google Inc. All rights reserved. +Copyright 2015 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. @@ -14,6 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package resourcequotausage provides Registry interface and it's REST -// implementation for storing ResourceQuotaUsage api objects. -package resourcequotausage +// Package fields implements a simple field system, parsing and matching +// selectors with sets of fields. +package fields diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/fields.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/fields.go new file mode 100644 index 000000000000..e2a6064655e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/fields.go @@ -0,0 +1,62 @@ +/* +Copyright 2015 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 fields + +import ( + "sort" + "strings" +) + +// Fields allows you to present fields independently from their storage. +type Fields interface { + // Has returns whether the provided field exists. + Has(field string) (exists bool) + + // Get returns the value for the provided field. + Get(field string) (value string) +} + +// Set is a map of field:value. It implements Fields. +type Set map[string]string + +// String returns all fields listed as a human readable string. +// Conveniently, exactly the format that ParseSelector takes. +func (ls Set) String() string { + selector := make([]string, 0, len(ls)) + for key, value := range ls { + selector = append(selector, key+"="+value) + } + // Sort for determinism. + sort.StringSlice(selector).Sort() + return strings.Join(selector, ",") +} + +// Has returns whether the provided field exists in the map. +func (ls Set) Has(field string) bool { + _, exists := ls[field] + return exists +} + +// Get returns the value in the map for the provided field. +func (ls Set) Get(field string) string { + return ls[field] +} + +// AsSelector converts fields into a selectors. +func (ls Set) AsSelector() Selector { + return SelectorFromSet(ls) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/fields_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/fields_test.go new file mode 100644 index 000000000000..c41c8e9876ae --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/fields_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2015 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 fields + +import ( + "testing" +) + +func matches(t *testing.T, ls Set, want string) { + if ls.String() != want { + t.Errorf("Expected '%s', but got '%s'", want, ls.String()) + } +} + +func TestSetString(t *testing.T) { + matches(t, Set{"x": "y"}, "x=y") + matches(t, Set{"foo": "bar"}, "foo=bar") + matches(t, Set{"foo": "bar", "baz": "qup"}, "baz=qup,foo=bar") +} + +func TestFieldHas(t *testing.T) { + fieldHasTests := []struct { + Ls Fields + Key string + Has bool + }{ + {Set{"x": "y"}, "x", true}, + {Set{"x": ""}, "x", true}, + {Set{"x": "y"}, "foo", false}, + } + for _, lh := range fieldHasTests { + if has := lh.Ls.Has(lh.Key); has != lh.Has { + t.Errorf("%#v.Has(%#v) => %v, expected %v", lh.Ls, lh.Key, has, lh.Has) + } + } +} + +func TestFieldGet(t *testing.T) { + ls := Set{"x": "y"} + if ls.Get("x") != "y" { + t.Errorf("Set.Get is broken") + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/selector.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/selector.go new file mode 100644 index 000000000000..e0ff08307492 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/selector.go @@ -0,0 +1,217 @@ +/* +Copyright 2015 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 fields + +import ( + "fmt" + "sort" + "strings" +) + +// Selector represents a field selector. +type Selector interface { + // Matches returns true if this selector matches the given set of fields. + Matches(Fields) bool + + // Empty returns true if this selector does not restrict the selection space. + Empty() bool + + // RequiresExactMatch allows a caller to introspect whether a given selector + // requires a single specific field to be set, and if so returns the value it + // requires. + RequiresExactMatch(field string) (value string, found bool) + + // String returns a human readable string that represents this selector. + String() string +} + +// Everything returns a selector that matches all fields. +func Everything() Selector { + return andTerm{} +} + +type hasTerm struct { + field, value string +} + +func (t *hasTerm) Matches(ls Fields) bool { + return ls.Get(t.field) == t.value +} + +func (t *hasTerm) Empty() bool { + return false +} + +func (t *hasTerm) RequiresExactMatch(field string) (value string, found bool) { + if t.field == field { + return t.value, true + } + return "", false +} + +func (t *hasTerm) String() string { + return fmt.Sprintf("%v=%v", t.field, t.value) +} + +type notHasTerm struct { + field, value string +} + +func (t *notHasTerm) Matches(ls Fields) bool { + return ls.Get(t.field) != t.value +} + +func (t *notHasTerm) Empty() bool { + return false +} + +func (t *notHasTerm) RequiresExactMatch(field string) (value string, found bool) { + return "", false +} + +func (t *notHasTerm) String() string { + return fmt.Sprintf("%v!=%v", t.field, t.value) +} + +type andTerm []Selector + +func (t andTerm) Matches(ls Fields) bool { + for _, q := range t { + if !q.Matches(ls) { + return false + } + } + return true +} + +func (t andTerm) Empty() bool { + if t == nil { + return true + } + if len([]Selector(t)) == 0 { + return true + } + for i := range t { + if !t[i].Empty() { + return false + } + } + return true +} + +func (t andTerm) RequiresExactMatch(field string) (string, bool) { + if t == nil || len([]Selector(t)) == 0 { + return "", false + } + for i := range t { + if value, found := t[i].RequiresExactMatch(field); found { + return value, found + } + } + return "", false +} + +func (t andTerm) String() string { + var terms []string + for _, q := range t { + terms = append(terms, q.String()) + } + return strings.Join(terms, ",") +} + +// SelectorFromSet returns a Selector which will match exactly the given Set. A +// nil Set is considered equivalent to Everything(). +func SelectorFromSet(ls Set) Selector { + if ls == nil { + return Everything() + } + items := make([]Selector, 0, len(ls)) + for field, value := range ls { + items = append(items, &hasTerm{field: field, value: value}) + } + if len(items) == 1 { + return items[0] + } + return andTerm(items) +} + +// ParseSelector takes a string representing a selector and returns an +// object suitable for matching, or an error. +func ParseSelector(selector string) (Selector, error) { + return parseSelector(selector, + func(lhs, rhs string) (newLhs, newRhs string, err error) { + return lhs, rhs, nil + }) +} + +// Parses the selector and runs them through the given TransformFunc. +func ParseAndTransformSelector(selector string, fn TransformFunc) (Selector, error) { + return parseSelector(selector, fn) +} + +// Function to transform selectors. +type TransformFunc func(field, value string) (newField, newValue string, err error) + +func try(selectorPiece, op string) (lhs, rhs string, ok bool) { + pieces := strings.Split(selectorPiece, op) + if len(pieces) == 2 { + return pieces[0], pieces[1], true + } + return "", "", false +} + +func parseSelector(selector string, fn TransformFunc) (Selector, error) { + parts := strings.Split(selector, ",") + sort.StringSlice(parts).Sort() + var items []Selector + for _, part := range parts { + if part == "" { + continue + } + if lhs, rhs, ok := try(part, "!="); ok { + lhs, rhs, err := fn(lhs, rhs) + if err != nil { + return nil, err + } + items = append(items, ¬HasTerm{field: lhs, value: rhs}) + } else if lhs, rhs, ok := try(part, "=="); ok { + lhs, rhs, err := fn(lhs, rhs) + if err != nil { + return nil, err + } + items = append(items, &hasTerm{field: lhs, value: rhs}) + } else if lhs, rhs, ok := try(part, "="); ok { + lhs, rhs, err := fn(lhs, rhs) + if err != nil { + return nil, err + } + items = append(items, &hasTerm{field: lhs, value: rhs}) + } else { + return nil, fmt.Errorf("invalid selector: '%s'; can't understand '%s'", selector, part) + } + } + if len(items) == 1 { + return items[0], nil + } + return andTerm(items), nil +} + +// OneTermEqualSelector returns an object that matches objects where one field/field equals one value. +// Cannot return an error. +func OneTermEqualSelector(k, v string) Selector { + return &hasTerm{field: k, value: v} +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/selector_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/selector_test.go new file mode 100644 index 000000000000..56431b740a7d --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/fields/selector_test.go @@ -0,0 +1,208 @@ +/* +Copyright 2015 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 fields + +import ( + "testing" +) + +func TestSelectorParse(t *testing.T) { + testGoodStrings := []string{ + "x=a,y=b,z=c", + "", + "x!=a,y=b", + } + testBadStrings := []string{ + "x=a||y=b", + "x==a==b", + } + for _, test := range testGoodStrings { + lq, err := ParseSelector(test) + if err != nil { + t.Errorf("%v: error %v (%#v)\n", test, err, err) + } + if test != lq.String() { + t.Errorf("%v restring gave: %v\n", test, lq.String()) + } + } + for _, test := range testBadStrings { + _, err := ParseSelector(test) + if err == nil { + t.Errorf("%v: did not get expected error\n", test) + } + } +} + +func TestDeterministicParse(t *testing.T) { + s1, err := ParseSelector("x=a,a=x") + s2, err2 := ParseSelector("a=x,x=a") + if err != nil || err2 != nil { + t.Errorf("Unexpected parse error") + } + if s1.String() != s2.String() { + t.Errorf("Non-deterministic parse") + } +} + +func expectMatch(t *testing.T, selector string, ls Set) { + lq, err := ParseSelector(selector) + if err != nil { + t.Errorf("Unable to parse %v as a selector\n", selector) + return + } + if !lq.Matches(ls) { + t.Errorf("Wanted %s to match '%s', but it did not.\n", selector, ls) + } +} + +func expectNoMatch(t *testing.T, selector string, ls Set) { + lq, err := ParseSelector(selector) + if err != nil { + t.Errorf("Unable to parse %v as a selector\n", selector) + return + } + if lq.Matches(ls) { + t.Errorf("Wanted '%s' to not match '%s', but it did.", selector, ls) + } +} + +func TestEverything(t *testing.T) { + if !Everything().Matches(Set{"x": "y"}) { + t.Errorf("Nil selector didn't match") + } + if !Everything().Empty() { + t.Errorf("Everything was not empty") + } +} + +func TestSelectorMatches(t *testing.T) { + expectMatch(t, "", Set{"x": "y"}) + expectMatch(t, "x=y", Set{"x": "y"}) + expectMatch(t, "x=y,z=w", Set{"x": "y", "z": "w"}) + expectMatch(t, "x!=y,z!=w", Set{"x": "z", "z": "a"}) + expectMatch(t, "notin=in", Set{"notin": "in"}) // in and notin in exactMatch + expectNoMatch(t, "x=y", Set{"x": "z"}) + expectNoMatch(t, "x=y,z=w", Set{"x": "w", "z": "w"}) + expectNoMatch(t, "x!=y,z!=w", Set{"x": "z", "z": "w"}) + + labelset := Set{ + "foo": "bar", + "baz": "blah", + } + expectMatch(t, "foo=bar", labelset) + expectMatch(t, "baz=blah", labelset) + expectMatch(t, "foo=bar,baz=blah", labelset) + expectNoMatch(t, "foo=blah", labelset) + expectNoMatch(t, "baz=bar", labelset) + expectNoMatch(t, "foo=bar,foobar=bar,baz=blah", labelset) +} + +func TestOneTermEqualSelector(t *testing.T) { + if !OneTermEqualSelector("x", "y").Matches(Set{"x": "y"}) { + t.Errorf("No match when match expected.") + } + if OneTermEqualSelector("x", "y").Matches(Set{"x": "z"}) { + t.Errorf("Match when none expected.") + } +} + +func expectMatchDirect(t *testing.T, selector, ls Set) { + if !SelectorFromSet(selector).Matches(ls) { + t.Errorf("Wanted %s to match '%s', but it did not.\n", selector, ls) + } +} + +func expectNoMatchDirect(t *testing.T, selector, ls Set) { + if SelectorFromSet(selector).Matches(ls) { + t.Errorf("Wanted '%s' to not match '%s', but it did.", selector, ls) + } +} + +func TestSetMatches(t *testing.T) { + labelset := Set{ + "foo": "bar", + "baz": "blah", + } + expectMatchDirect(t, Set{}, labelset) + expectMatchDirect(t, Set{"foo": "bar"}, labelset) + expectMatchDirect(t, Set{"baz": "blah"}, labelset) + expectMatchDirect(t, Set{"foo": "bar", "baz": "blah"}, labelset) + expectNoMatchDirect(t, Set{"foo": "=blah"}, labelset) + expectNoMatchDirect(t, Set{"baz": "=bar"}, labelset) + expectNoMatchDirect(t, Set{"foo": "=bar", "foobar": "bar", "baz": "blah"}, labelset) +} + +func TestNilMapIsValid(t *testing.T) { + selector := Set(nil).AsSelector() + if selector == nil { + t.Errorf("Selector for nil set should be Everything") + } + if !selector.Empty() { + t.Errorf("Selector for nil set should be Empty") + } +} + +func TestSetIsEmpty(t *testing.T) { + if !(Set{}).AsSelector().Empty() { + t.Errorf("Empty set should be empty") + } + if !(andTerm(nil)).Empty() { + t.Errorf("Nil andTerm should be empty") + } + if (&hasTerm{}).Empty() { + t.Errorf("hasTerm should not be empty") + } + if (¬HasTerm{}).Empty() { + t.Errorf("notHasTerm should not be empty") + } + if !(andTerm{andTerm{}}).Empty() { + t.Errorf("Nested andTerm should be empty") + } + if (andTerm{&hasTerm{"a", "b"}}).Empty() { + t.Errorf("Nested andTerm should not be empty") + } +} + +func TestRequiresExactMatch(t *testing.T) { + testCases := map[string]struct { + S Selector + Label string + Value string + Found bool + }{ + "empty set": {Set{}.AsSelector(), "test", "", false}, + "nil andTerm": {andTerm(nil), "test", "", false}, + "empty hasTerm": {&hasTerm{}, "test", "", false}, + "skipped hasTerm": {&hasTerm{"a", "b"}, "test", "", false}, + "valid hasTerm": {&hasTerm{"test", "b"}, "test", "b", true}, + "valid hasTerm no value": {&hasTerm{"test", ""}, "test", "", true}, + "valid notHasTerm": {¬HasTerm{"test", "b"}, "test", "", false}, + "valid notHasTerm no value": {¬HasTerm{"test", ""}, "test", "", false}, + "nested andTerm": {andTerm{andTerm{}}, "test", "", false}, + "nested andTerm matches": {andTerm{&hasTerm{"test", "b"}}, "test", "b", true}, + "andTerm with non-match": {andTerm{&hasTerm{}, &hasTerm{"test", "b"}}, "test", "b", true}, + } + for k, v := range testCases { + value, found := v.S.RequiresExactMatch(v.Label) + if value != v.Value { + t.Errorf("%s: expected value %s, got %s", k, v.Value, value) + } + if found != v.Found { + t.Errorf("%s: expected found %t, got %t", k, v.Found, found) + } + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/healthz/healthz.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/healthz/healthz.go index f4542e5d0b1a..2e8fda9b5f77 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/healthz/healthz.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/healthz/healthz.go @@ -17,25 +17,112 @@ limitations under the License. package healthz import ( + "bytes" + "fmt" "net/http" ) +// HealthzChecker is a named healthz check. +type HealthzChecker interface { + Name() string + Check(req *http.Request) error +} + +// DefaultHealthz installs the default healthz check to the http.DefaultServeMux. +func DefaultHealthz(checks ...HealthzChecker) { + InstallHandler(http.DefaultServeMux, checks...) +} + +// PingHealthz returns true automatically when checked +var PingHealthz HealthzChecker = ping{} + +// ping implements the simplest possible health checker. +type ping struct{} + +func (ping) Name() string { + return "ping" +} + +// PingHealthz is a health check that returns true. +func (ping) Check(_ *http.Request) error { + return nil +} + +// NamedCheck returns a health checker for the given name and function. +func NamedCheck(name string, check func(r *http.Request) error) HealthzChecker { + return &healthzCheck{name, check} +} + +// InstallHandler registers a handler for health checking on the path "/healthz" to mux. +func InstallHandler(mux mux, checks ...HealthzChecker) { + if len(checks) == 0 { + checks = []HealthzChecker{PingHealthz} + } + mux.Handle("/healthz", handleRootHealthz(checks...)) + for _, check := range checks { + mux.Handle(fmt.Sprintf("/healthz/%v", check.Name()), adaptCheckToHandler(check.Check)) + } +} + // mux is an interface describing the methods InstallHandler requires. type mux interface { - HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) + Handle(pattern string, handler http.Handler) } -func init() { - http.HandleFunc("/healthz", handleHealthz) +// healthzCheck implements HealthzChecker on an arbitrary name and check function. +type healthzCheck struct { + name string + check func(r *http.Request) error } -func handleHealthz(w http.ResponseWriter, r *http.Request) { - // TODO Support user supplied health functions too. - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) +var _ HealthzChecker = &healthzCheck{} + +func (c *healthzCheck) Name() string { + return c.name } -// InstallHandler registers a handler for health checking on the path "/healthz" to mux. -func InstallHandler(mux mux) { - mux.HandleFunc("/healthz", handleHealthz) +func (c *healthzCheck) Check(r *http.Request) error { + return c.check(r) +} + +// handleRootHealthz returns an http.HandlerFunc that serves the provided checks. +func handleRootHealthz(checks ...HealthzChecker) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + failed := false + var verboseOut bytes.Buffer + for _, check := range checks { + err := check.Check(r) + if err != nil { + fmt.Fprintf(&verboseOut, "[-]%v failed: %v\n", check.Name(), err) + failed = true + } else { + fmt.Fprintf(&verboseOut, "[+]%v ok\n", check.Name()) + } + } + // always be verbose on failure + if failed { + http.Error(w, fmt.Sprintf("%vhealthz check failed", verboseOut.String()), http.StatusInternalServerError) + return + } + + if _, found := r.URL.Query()["verbose"]; !found { + fmt.Fprint(w, "ok") + return + } + + verboseOut.WriteTo(w) + fmt.Fprint(w, "healthz check passed\n") + }) +} + +// adaptCheckToHandler returns an http.HandlerFunc that serves the provided checks. +func adaptCheckToHandler(c func(r *http.Request) error) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := c(r) + if err != nil { + http.Error(w, fmt.Sprintf("Internal server error: %v", err), http.StatusInternalServerError) + } else { + fmt.Fprint(w, "ok") + } + }) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/healthz/healthz_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/healthz/healthz_test.go index 7203443e52a2..432750861fb4 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/healthz/healthz_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/healthz/healthz_test.go @@ -17,6 +17,8 @@ limitations under the License. package healthz import ( + "errors" + "fmt" "net/http" "net/http/httptest" "testing" @@ -38,3 +40,43 @@ func TestInstallHandler(t *testing.T) { t.Errorf("Expected %v, got %v", "ok", w.Body.String()) } } + +func TestMulitipleChecks(t *testing.T) { + tests := []struct { + path string + expectedResponse string + expectedStatus int + addBadCheck bool + }{ + {"/healthz?verbose", "[+]ping ok\nhealthz check passed\n", http.StatusOK, false}, + {"/healthz/ping", "ok", http.StatusOK, false}, + {"/healthz", "ok", http.StatusOK, false}, + {"/healthz?verbose", "[+]ping ok\n[-]bad failed: this will fail\nhealthz check failed\n", http.StatusInternalServerError, true}, + {"/healthz/ping", "ok", http.StatusOK, true}, + {"/healthz/bad", "Internal server error: this will fail\n", http.StatusInternalServerError, true}, + {"/healthz", "[+]ping ok\n[-]bad failed: this will fail\nhealthz check failed\n", http.StatusInternalServerError, true}, + } + + for i, test := range tests { + mux := http.NewServeMux() + checks := []HealthzChecker{PingHealthz} + if test.addBadCheck { + checks = append(checks, NamedCheck("bad", func(_ *http.Request) error { + return errors.New("this will fail") + })) + } + InstallHandler(mux, checks...) + req, err := http.NewRequest("GET", fmt.Sprintf("http://example.com%v", test.path), nil) + if err != nil { + t.Fatalf("case[%d] Unexpected error: %v", i, err) + } + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + if w.Code != test.expectedStatus { + t.Errorf("case[%d] Expected: %v, got: %v", i, test.expectedStatus, w.Code) + } + if w.Body.String() != test.expectedResponse { + t.Errorf("case[%d] Expected:\n%v\ngot:\n%v\n", i, test.expectedResponse, w.Body.String()) + } + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/httplog/log.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/httplog/log.go index ec26de718e9c..619d22d35321 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/httplog/log.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/httplog/log.go @@ -74,7 +74,7 @@ func (passthroughLogger) Addf(format string, data ...interface{}) { // DefaultStacktracePred is the default implementation of StacktracePred. func DefaultStacktracePred(status int) bool { - return (status < http.StatusOK || status >= http.StatusBadRequest) && status != http.StatusSwitchingProtocols + return (status < http.StatusOK || status >= http.StatusInternalServerError) && status != http.StatusSwitchingProtocols } // NewLogged turns a normal response writer into a logged response writer. @@ -155,7 +155,7 @@ func (rl *respLogger) Addf(format string, data ...interface{}) { func (rl *respLogger) Log() { latency := time.Since(rl.startTime) if glog.V(2) { - glog.InfoDepth(1, fmt.Sprintf("%s %s: (%v) %v%v%v", rl.req.Method, rl.req.RequestURI, latency, rl.status, rl.statusStack, rl.addedInfo)) + glog.InfoDepth(1, fmt.Sprintf("%s %s: (%v) %v%v%v [%s %s]", rl.req.Method, rl.req.RequestURI, latency, rl.status, rl.statusStack, rl.addedInfo, rl.req.Header["User-Agent"], rl.req.RemoteAddr)) } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/apiversions.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/apiversions.go new file mode 100644 index 000000000000..466300b7c3ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/apiversions.go @@ -0,0 +1,48 @@ +/* +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/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" +) + +func (f *Factory) NewCmdApiVersions(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "apiversions", + Short: "Print available API versions.", + Run: func(cmd *cobra.Command, args []string) { + err := RunApiVersions(f, out) + util.CheckErr(err) + }, + } + return cmd +} + +func RunApiVersions(f *Factory, out io.Writer) error { + client, err := f.Client() + if err != nil { + return err + } + + kubectl.GetApiVersions(out, client) + return nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/clusterinfo.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/clusterinfo.go new file mode 100644 index 000000000000..c502804fc609 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/clusterinfo.go @@ -0,0 +1,79 @@ +/* +Copyright 2015 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" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" + + "github.com/spf13/cobra" +) + +func (f *Factory) NewCmdClusterInfo(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "clusterinfo", + Short: "Display cluster info", + Long: "Display addresses of the master and services with label kubernetes.io/cluster-service=true", + Run: func(cmd *cobra.Command, args []string) { + err := RunClusterInfo(f, out, cmd) + util.CheckErr(err) + }, + } + return cmd +} + +func RunClusterInfo(factory *Factory, out io.Writer, cmd *cobra.Command) error { + client, err := factory.ClientConfig() + if err != nil { + return err + } + fmt.Fprintf(out, "Kubernetes master is running at %v\n", client.Host) + + mapper, typer := factory.Object() + cmdNamespace, err := factory.DefaultNamespace() + if err != nil { + return err + } + + // TODO: use generalized labels once they are implemented (#341) + b := resource.NewBuilder(mapper, typer, factory.ClientMapperForCommand(cmd)). + NamespaceParam(cmdNamespace).DefaultNamespace(). + SelectorParam("kubernetes.io/cluster-service=true"). + ResourceTypeOrNameArgs(false, []string{"services"}...). + Latest() + b.Do().Visit(func(r *resource.Info) error { + services := r.Object.(*api.ServiceList).Items + for _, service := range services { + splittedLink := strings.Split(strings.Split(service.ObjectMeta.SelfLink, "?")[0], "/") + // insert "proxy" into the link + splittedLink = append(splittedLink, "") + copy(splittedLink[4:], splittedLink[3:]) + splittedLink[3] = "proxy" + link := strings.Join(splittedLink, "/") + fmt.Fprintf(out, "%v is running at %v%v/\n", service.ObjectMeta.Labels["name"], client.Host, link) + } + return nil + }) + return nil + + // TODO: consider printing more information about cluster +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd.go index 12cb0f88ad75..3ac3cd6059c2 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd.go @@ -34,7 +34,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - "github.com/golang/glog" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -51,26 +50,26 @@ type Factory struct { flags *pflag.FlagSet // Returns interfaces for dealing with arbitrary runtime.Objects. - Object func(cmd *cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) + Object func() (meta.RESTMapper, runtime.ObjectTyper) // Returns a client for accessing Kubernetes resources or an error. - Client func(cmd *cobra.Command) (*client.Client, error) + Client func() (*client.Client, error) // Returns a client.Config for accessing the Kubernetes server. - ClientConfig func(cmd *cobra.Command) (*client.Config, error) + ClientConfig func() (*client.Config, error) // Returns a RESTClient for working with the specified RESTMapping or an error. This is intended // for working with arbitrary resources and is not guaranteed to point to a Kubernetes APIServer. - RESTClient func(cmd *cobra.Command, mapping *meta.RESTMapping) (resource.RESTClient, error) + RESTClient func(mapping *meta.RESTMapping) (resource.RESTClient, error) // Returns a Describer for displaying the specified RESTMapping type or an error. - Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) + Describer func(mapping *meta.RESTMapping) (kubectl.Describer, error) // Returns a Printer for formatting objects of the given type or an error. - Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) + Printer func(mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) // Returns a Resizer for changing the size of the specified RESTMapping type or an error - Resizer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Resizer, error) + Resizer func(mapping *meta.RESTMapping) (kubectl.Resizer, error) // Returns a Reaper for gracefully shutting down resources. - Reaper func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Reaper, error) + Reaper func(mapping *meta.RESTMapping) (kubectl.Reaper, error) // Returns a schema that can validate objects stored on disk. - Validator func(*cobra.Command) (validation.Schema, error) + Validator func() (validation.Schema, error) // Returns the default namespace to use in cases where no other namespace is specified - DefaultNamespace func(cmd *cobra.Command) (string, error) + DefaultNamespace func() (string, error) } // NewFactory creates a factory with the default Kubernetes resources defined @@ -95,27 +94,27 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { clients: clients, flags: flags, - Object: func(cmd *cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) { + Object: func() (meta.RESTMapper, runtime.ObjectTyper) { cfg, err := clientConfig.ClientConfig() - checkErr(err) + cmdutil.CheckErr(err) cmdApiVersion := cfg.Version return kubectl.OutputVersionMapper{mapper, cmdApiVersion}, api.Scheme }, - Client: func(cmd *cobra.Command) (*client.Client, error) { + Client: func() (*client.Client, error) { return clients.ClientForVersion("") }, - ClientConfig: func(cmd *cobra.Command) (*client.Config, error) { + ClientConfig: func() (*client.Config, error) { return clients.ClientConfigForVersion("") }, - RESTClient: func(cmd *cobra.Command, mapping *meta.RESTMapping) (resource.RESTClient, error) { + RESTClient: func(mapping *meta.RESTMapping) (resource.RESTClient, error) { client, err := clients.ClientForVersion(mapping.APIVersion) if err != nil { return nil, err } return client.RESTClient, nil }, - Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { + Describer: func(mapping *meta.RESTMapping) (kubectl.Describer, error) { client, err := clients.ClientForVersion(mapping.APIVersion) if err != nil { return nil, err @@ -126,25 +125,25 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return describer, nil }, - Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { + Printer: func(mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return kubectl.NewHumanReadablePrinter(noHeaders), nil }, - Resizer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Resizer, error) { + Resizer: func(mapping *meta.RESTMapping) (kubectl.Resizer, error) { client, err := clients.ClientForVersion(mapping.APIVersion) if err != nil { return nil, err } return kubectl.ResizerFor(mapping.Kind, client) }, - Reaper: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Reaper, error) { + Reaper: func(mapping *meta.RESTMapping) (kubectl.Reaper, error) { client, err := clients.ClientForVersion(mapping.APIVersion) if err != nil { return nil, err } return kubectl.ReaperFor(mapping.Kind, client) }, - Validator: func(cmd *cobra.Command) (validation.Schema, error) { - if cmdutil.GetFlagBool(cmd, "validate") { + Validator: func() (validation.Schema, error) { + if flags.Lookup("validate").Value.String() == "true" { client, err := clients.ClientForVersion("") if err != nil { return nil, err @@ -153,7 +152,7 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return validation.NullSchema{}, nil }, - DefaultNamespace: func(cmd *cobra.Command) (string, error) { + DefaultNamespace: func() (string, error) { return clientConfig.Namespace() }, } @@ -168,6 +167,12 @@ func (f *Factory) BindFlags(flags *pflag.FlagSet) { // pflags currently. See https://github.com/spf13/cobra/issues/44. util.AddPFlagSetToPFlagSet(pflag.CommandLine, flags) + // Hack for global access to validation flag. + // TODO: Refactor out after configuration flag overhaul. + if f.flags.Lookup("validate") == nil { + f.flags.Bool("validate", false, "If true, use a schema to validate the input before sending it") + } + if f.flags != nil { f.flags.VisitAll(func(flag *pflag.Flag) { flags.AddFlag(flag) @@ -179,7 +184,6 @@ func (f *Factory) BindFlags(flags *pflag.FlagSet) { // TODO Add a verbose flag that turns on glog logging. Probably need a way // to do that automatically for every subcommand. flags.BoolVar(&f.clients.matchVersion, FlagMatchBinaryVersion, false, "Require server version to match client version") - flags.Bool("validate", false, "If true, use a schema to validate the input before sending it") } // NewKubectlCommand creates the `kubectl` command and its nested children. @@ -197,6 +201,8 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, f.BindFlags(cmds.PersistentFlags()) cmds.AddCommand(f.NewCmdVersion(out)) + cmds.AddCommand(f.NewCmdApiVersions(out)) + cmds.AddCommand(f.NewCmdClusterInfo(out)) cmds.AddCommand(f.NewCmdProxy(out)) cmds.AddCommand(f.NewCmdGet(out)) @@ -225,7 +231,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, // PrintObject prints an api object given command line flags to modify the output format func (f *Factory) PrintObject(cmd *cobra.Command, obj runtime.Object, out io.Writer) error { - mapper, _ := f.Object(cmd) + mapper, _ := f.Object() _, kind, err := api.Scheme.ObjectVersionAndKind(obj) if err != nil { return err @@ -251,8 +257,10 @@ func (f *Factory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMappin return nil, err } if ok { - clientConfig, err := f.ClientConfig(cmd) - checkErr(err) + clientConfig, err := f.ClientConfig() + if err != nil { + return nil, err + } defaultVersion := clientConfig.Version version := cmdutil.OutputVersion(cmd, defaultVersion) @@ -264,7 +272,7 @@ func (f *Factory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMappin } printer = kubectl.NewVersionedPrinter(printer, mapping.ObjectConvertor, version) } else { - printer, err = f.Printer(cmd, mapping, cmdutil.GetFlagBool(cmd, "no-headers")) + printer, err = f.Printer(mapping, cmdutil.GetFlagBool(cmd, "no-headers")) if err != nil { return nil, err } @@ -275,7 +283,7 @@ func (f *Factory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMappin // ClientMapperForCommand returns a ClientMapper for the given command and factory. func (f *Factory) ClientMapperForCommand(cmd *cobra.Command) resource.ClientMapper { return resource.ClientMapperFunc(func(mapping *meta.RESTMapping) (resource.RESTClient, error) { - return f.RESTClient(cmd, mapping) + return f.RESTClient(mapping) }) } @@ -327,18 +335,6 @@ func DefaultClientConfig(flags *pflag.FlagSet) clientcmd.ClientConfig { return clientConfig } -func checkErr(err error) { - if err != nil { - glog.FatalDepth(1, err.Error()) - } -} - -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() } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd_test.go index e813e9f74142..410ec4901427 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/cmd_test.go @@ -33,8 +33,6 @@ import ( . "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - - "github.com/spf13/cobra" ) type internalType struct { @@ -120,25 +118,25 @@ func NewTestFactory() (*Factory, *testFactory, runtime.Codec) { Typer: scheme, } return &Factory{ - Object: func(*cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) { + Object: func() (meta.RESTMapper, runtime.ObjectTyper) { return t.Mapper, t.Typer }, - RESTClient: func(*cobra.Command, *meta.RESTMapping) (resource.RESTClient, error) { + RESTClient: func(*meta.RESTMapping) (resource.RESTClient, error) { return t.Client, t.Err }, - Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { + Describer: func(*meta.RESTMapping) (kubectl.Describer, error) { return t.Describer, t.Err }, - Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { + Printer: func(mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return t.Printer, t.Err }, - Validator: func(cmd *cobra.Command) (validation.Schema, error) { + Validator: func() (validation.Schema, error) { return t.Validator, t.Err }, - DefaultNamespace: func(cmd *cobra.Command) (string, error) { + DefaultNamespace: func() (string, error) { return t.Namespace, t.Err }, - ClientConfig: func(cmd *cobra.Command) (*client.Config, error) { + ClientConfig: func() (*client.Config, error) { return t.ClientConfig, t.Err }, }, t, codec @@ -149,25 +147,25 @@ func NewAPIFactory() (*Factory, *testFactory, runtime.Codec) { Validator: validation.NullSchema{}, } return &Factory{ - Object: func(*cobra.Command) (meta.RESTMapper, runtime.ObjectTyper) { + Object: func() (meta.RESTMapper, runtime.ObjectTyper) { return latest.RESTMapper, api.Scheme }, - RESTClient: func(*cobra.Command, *meta.RESTMapping) (resource.RESTClient, error) { + RESTClient: func(*meta.RESTMapping) (resource.RESTClient, error) { return t.Client, t.Err }, - Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { + Describer: func(*meta.RESTMapping) (kubectl.Describer, error) { return t.Describer, t.Err }, - Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { + Printer: func(mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return t.Printer, t.Err }, - Validator: func(cmd *cobra.Command) (validation.Schema, error) { + Validator: func() (validation.Schema, error) { return t.Validator, t.Err }, - DefaultNamespace: func(cmd *cobra.Command) (string, error) { + DefaultNamespace: func() (string, error) { return t.Namespace, t.Err }, - ClientConfig: func(cmd *cobra.Command) (*client.Config, error) { + ClientConfig: func() (*client.Config, error) { return t.ClientConfig, t.Err }, }, t, latest.Codec @@ -194,7 +192,7 @@ func TestClientVersions(t *testing.T) { mapping := &meta.RESTMapping{ APIVersion: version, } - c, err := f.RESTClient(nil, mapping) + c, err := f.RESTClient(mapping) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/config.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/config.go index dca868c0e30b..8ec9632809a0 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/config.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/config.go @@ -40,7 +40,7 @@ func NewCmdConfig(out io.Writer) *cobra.Command { pathOptions := &pathOptions{} cmd := &cobra.Command{ - Use: "config ", + Use: "config SUBCOMMAND", Short: "config modifies .kubeconfig files", Long: `config modifies .kubeconfig files using subcommands like "kubectl config set current-context my-context"`, Run: func(cmd *cobra.Command, args []string) { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/config_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/config_test.go index 75bfe1b88321..a528ae8aa273 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/config_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/config_test.go @@ -18,7 +18,6 @@ package config import ( "bytes" - "fmt" "io/ioutil" "os" "reflect" @@ -48,36 +47,6 @@ type configCommandTest struct { expectedOutputs []string } -func ExampleView() { - expectedConfig := newRedFederalCowHammerConfig() - test := configCommandTest{ - args: []string{"view"}, - startingConfig: newRedFederalCowHammerConfig(), - expectedConfig: expectedConfig, - } - - output := test.run(nil) - fmt.Printf("%v", output) - // Output: - // apiVersion: v1 - // clusters: - // - cluster: - // server: http://cow.org:8080 - // name: cow-cluster - // contexts: - // - context: - // cluster: cow-cluster - // user: red-user - // name: federal-context - // current-context: "" - // kind: Config - // preferences: {} - // users: - // - name: red-user - // user: - // token: red-token -} - func TestSetCurrentContext(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() expectedConfig.CurrentContext = "the-new-context" @@ -198,6 +167,56 @@ func TestAdditionalAuth(t *testing.T) { test.run(t) } +func TestEmbedClientCert(t *testing.T) { + fakeCertFile, _ := ioutil.TempFile("", "") + defer os.Remove(fakeCertFile.Name()) + fakeData := []byte("fake-data") + ioutil.WriteFile(fakeCertFile.Name(), fakeData, 0600) + expectedConfig := newRedFederalCowHammerConfig() + authInfo := clientcmdapi.NewAuthInfo() + authInfo.ClientCertificateData = fakeData + expectedConfig.AuthInfos["another-user"] = *authInfo + + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=" + fakeCertFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestEmbedClientKey(t *testing.T) { + fakeKeyFile, _ := ioutil.TempFile("", "") + defer os.Remove(fakeKeyFile.Name()) + fakeData := []byte("fake-data") + ioutil.WriteFile(fakeKeyFile.Name(), fakeData, 0600) + expectedConfig := newRedFederalCowHammerConfig() + authInfo := clientcmdapi.NewAuthInfo() + authInfo.ClientKeyData = fakeData + expectedConfig.AuthInfos["another-user"] = *authInfo + + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagKeyFile + "=" + fakeKeyFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestEmbedNoKeyOrCertDisallowed(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagEmbedCerts + "=true"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + expectedOutputs: []string{"--client-certificate", "--client-key", "embed"}, + } + + test.run(t) +} + func TestEmptyTokenAndCertAllowed(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() authInfo := clientcmdapi.NewAuthInfo() @@ -406,6 +425,45 @@ func TestInsecureClearsCA(t *testing.T) { test.run(t) } +func TestCADataClearsCA(t *testing.T) { + fakeCAFile, _ := ioutil.TempFile("", "") + defer os.Remove(fakeCAFile.Name()) + fakeData := []byte("cadata") + ioutil.WriteFile(fakeCAFile.Name(), fakeData, 0600) + + clusterInfoWithCAData := clientcmdapi.NewCluster() + clusterInfoWithCAData.CertificateAuthorityData = fakeData + + clusterInfoWithCA := clientcmdapi.NewCluster() + clusterInfoWithCA.CertificateAuthority = "cafile" + + startingConfig := newRedFederalCowHammerConfig() + startingConfig.Clusters["another-cluster"] = *clusterInfoWithCA + + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.Clusters["another-cluster"] = *clusterInfoWithCAData + + test := configCommandTest{ + args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=" + fakeCAFile.Name(), "--" + clientcmd.FlagEmbedCerts + "=true"}, + startingConfig: startingConfig, + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestEmbedNoCADisallowed(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + test := configCommandTest{ + args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagEmbedCerts + "=true"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + expectedOutputs: []string{"--certificate-authority", "embed"}, + } + + test.run(t) +} + func TestCAAndInsecureDisallowed(t *testing.T) { test := configCommandTest{ args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=cafile", "--" + clientcmd.FlagInsecure + "=true"}, @@ -571,7 +629,7 @@ func testConfigCommand(args []string, startingConfig clientcmdapi.Config) (strin return buf.String(), *config } -func (test configCommandTest) run(t *testing.T) string { +func (test configCommandTest) run(t *testing.T) { out, actualConfig := testConfigCommand(test.args, test.startingConfig) testSetNilMapsToEmpties(reflect.ValueOf(&test.expectedConfig)) @@ -587,8 +645,6 @@ func (test configCommandTest) run(t *testing.T) string { t.Errorf("expected '%s' in output, got '%s'", expectedOutput, out) } } - - return out } func testSetNilMapsToEmpties(curr reflect.Value) { actualCurrValue := curr diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_authinfo.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_authinfo.go index 22860f1c5944..8253e350a5a6 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_authinfo.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_authinfo.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "strings" "github.com/spf13/cobra" @@ -38,21 +39,11 @@ type createAuthInfoOptions struct { token util.StringFlag username util.StringFlag password util.StringFlag + embedCertData util.BoolFlag } -func NewCmdConfigSetAuthInfo(out io.Writer, pathOptions *pathOptions) *cobra.Command { - options := &createAuthInfoOptions{pathOptions: pathOptions} - - cmd := &cobra.Command{ - Use: fmt.Sprintf("set-credentials name [--%v=authfile] [--%v=certfile] [--%v=keyfile] [--%v=bearer_token] [--%v=basic_user] [--%v=basic_password]", clientcmd.FlagAuthPath, clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword), - Short: "Sets a user entry in .kubeconfig", - Long: fmt.Sprintf(`Sets a user entry in .kubeconfig - - Specifying a name that already exists will merge new fields on top of existing - values. For example, the following only sets the "client-key" field on the - "cluster-admin" entry, without touching other values: - - set-credentials cluster-admin --client-key=~/.kube/admin.key +var create_authinfo_long = fmt.Sprintf(`Sets a user entry in .kubeconfig +Specifying a name that already exists will merge new fields on top of existing values. Client-certificate flags: --%v=certfile --%v=keyfile @@ -64,7 +55,26 @@ func NewCmdConfigSetAuthInfo(out io.Writer, pathOptions *pathOptions) *cobra.Com --%v=basic_user --%v=basic_password Bearer token and basic auth are mutually exclusive. -`, clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword), +`, clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword) + +const create_authinfo_example = `// Set only the "client-key" field on the "cluster-admin" +// entry, without touching other values: +$ kubectl set-credentials cluster-admin --client-key=~/.kube/admin.key + +// Set basic auth for the "cluster-admin" entry +$ kubectl set-credentials cluster-admin --username=admin --password=uXFGweU9l35qcif + +// Embed client certificate data in the "cluster-admin" entry +$ kubectl set-credentials cluster-admin --client-certificate=~/.kube/admin.crt --embed-certs=true` + +func NewCmdConfigSetAuthInfo(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &createAuthInfoOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: fmt.Sprintf("set-credentials NAME [--%v=/path/to/authfile] [--%v=path/to/certfile] [--%v=path/to/keyfile] [--%v=bearer_token] [--%v=basic_user] [--%v=basic_password]", clientcmd.FlagAuthPath, clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword), + Short: "Sets a user entry in .kubeconfig", + Long: create_authinfo_long, + Example: create_authinfo_example, Run: func(cmd *cobra.Command, args []string) { if !options.complete(cmd) { return @@ -78,11 +88,12 @@ func NewCmdConfigSetAuthInfo(out io.Writer, pathOptions *pathOptions) *cobra.Com } cmd.Flags().Var(&options.authPath, clientcmd.FlagAuthPath, clientcmd.FlagAuthPath+" for the user entry in .kubeconfig") - cmd.Flags().Var(&options.clientCertificate, clientcmd.FlagCertFile, clientcmd.FlagCertFile+" for the user entry in .kubeconfig") - cmd.Flags().Var(&options.clientKey, clientcmd.FlagKeyFile, clientcmd.FlagKeyFile+" for the user entry in .kubeconfig") + cmd.Flags().Var(&options.clientCertificate, clientcmd.FlagCertFile, "path to "+clientcmd.FlagCertFile+" for the user entry in .kubeconfig") + cmd.Flags().Var(&options.clientKey, clientcmd.FlagKeyFile, "path to "+clientcmd.FlagKeyFile+" for the user entry in .kubeconfig") cmd.Flags().Var(&options.token, clientcmd.FlagBearerToken, clientcmd.FlagBearerToken+" for the user entry in .kubeconfig") cmd.Flags().Var(&options.username, clientcmd.FlagUsername, clientcmd.FlagUsername+" for the user entry in .kubeconfig") cmd.Flags().Var(&options.password, clientcmd.FlagPassword, clientcmd.FlagPassword+" for the user entry in .kubeconfig") + cmd.Flags().Var(&options.embedCertData, clientcmd.FlagEmbedCerts, "embed client cert/key for the user entry in .kubeconfig") return cmd } @@ -120,15 +131,27 @@ func (o *createAuthInfoOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.Aut } if o.clientCertificate.Provided() { - modifiedAuthInfo.ClientCertificate = o.clientCertificate.Value() - if len(modifiedAuthInfo.ClientCertificate) > 0 { - modifiedAuthInfo.ClientCertificateData = nil + certPath := o.clientCertificate.Value() + if o.embedCertData.Value() { + modifiedAuthInfo.ClientCertificateData, _ = ioutil.ReadFile(certPath) + modifiedAuthInfo.ClientCertificate = "" + } else { + modifiedAuthInfo.ClientCertificate = certPath + if len(modifiedAuthInfo.ClientCertificate) > 0 { + modifiedAuthInfo.ClientCertificateData = nil + } } } if o.clientKey.Provided() { - modifiedAuthInfo.ClientKey = o.clientKey.Value() - if len(modifiedAuthInfo.ClientKey) > 0 { - modifiedAuthInfo.ClientKeyData = nil + keyPath := o.clientKey.Value() + if o.embedCertData.Value() { + modifiedAuthInfo.ClientKeyData, _ = ioutil.ReadFile(keyPath) + modifiedAuthInfo.ClientKey = "" + } else { + modifiedAuthInfo.ClientKey = keyPath + if len(modifiedAuthInfo.ClientKey) > 0 { + modifiedAuthInfo.ClientKeyData = nil + } } } @@ -185,6 +208,23 @@ func (o createAuthInfoOptions) validate() error { if len(methods) > 1 { return fmt.Errorf("You cannot specify more than one authentication method at the same time: %v", strings.Join(methods, ", ")) } + if o.embedCertData.Value() { + certPath := o.clientCertificate.Value() + keyPath := o.clientKey.Value() + if certPath == "" && keyPath == "" { + return fmt.Errorf("You must specify a --%s or --%s to embed", clientcmd.FlagCertFile, clientcmd.FlagKeyFile) + } + if certPath != "" { + if _, err := ioutil.ReadFile(certPath); err != nil { + return fmt.Errorf("Error reading %s data from %s: %v", clientcmd.FlagCertFile, certPath, err) + } + } + if keyPath != "" { + if _, err := ioutil.ReadFile(keyPath); err != nil { + return fmt.Errorf("Error reading %s data from %s: %v", clientcmd.FlagKeyFile, keyPath, err) + } + } + } return nil } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_cluster.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_cluster.go index a1a2bde67908..08c38361b20d 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_cluster.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_cluster.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "github.com/spf13/cobra" @@ -35,20 +36,30 @@ type createClusterOptions struct { apiVersion util.StringFlag insecureSkipTLSVerify util.BoolFlag certificateAuthority util.StringFlag + embedCAData util.BoolFlag } +const ( + create_cluster_long = `Sets a cluster entry in .kubeconfig. +Specifying a name that already exists will merge new fields on top of existing values for those fields.` + create_cluster_example = `// Set only the server field on the e2e cluster entry without touching other values. +$ kubectl config set-cluster e2e --server=https://1.2.3.4 + +// Embed certificate authority data for the e2e cluster entry +$ kubectl config set-cluster e2e --certificate-authority=~/.kube/e2e/kubernetes.ca.crt + +// Disable cert checking for the dev cluster entry +$ kubectl config set-cluster e2e --insecure-skip-tls-verify=true` +) + func NewCmdConfigSetCluster(out io.Writer, pathOptions *pathOptions) *cobra.Command { options := &createClusterOptions{pathOptions: pathOptions} cmd := &cobra.Command{ - Use: fmt.Sprintf("set-cluster name [--%v=server] [--%v=path/to/certficate/authority] [--%v=apiversion] [--%v=true]", clientcmd.FlagAPIServer, clientcmd.FlagCAFile, clientcmd.FlagAPIVersion, clientcmd.FlagInsecure), - Short: "Sets a cluster entry in .kubeconfig", - Long: `Sets a cluster entry in .kubeconfig - Specifying a name that already exists will merge new fields on top of existing values for those fields. - e.g. - kubectl config set-cluster e2e --certificate-authority=~/.kube/e2e/.kubernetes.ca.cert - only sets the certificate-authority field on the e2e cluster entry without touching other values. - `, + Use: fmt.Sprintf("set-cluster NAME [--%v=server] [--%v=path/to/certficate/authority] [--%v=apiversion] [--%v=true]", clientcmd.FlagAPIServer, clientcmd.FlagCAFile, clientcmd.FlagAPIVersion, clientcmd.FlagInsecure), + Short: "Sets a cluster entry in .kubeconfig", + Long: create_cluster_long, + Example: create_cluster_example, Run: func(cmd *cobra.Command, args []string) { if !options.complete(cmd) { return @@ -66,7 +77,8 @@ func NewCmdConfigSetCluster(out io.Writer, pathOptions *pathOptions) *cobra.Comm cmd.Flags().Var(&options.server, clientcmd.FlagAPIServer, clientcmd.FlagAPIServer+" for the cluster entry in .kubeconfig") cmd.Flags().Var(&options.apiVersion, clientcmd.FlagAPIVersion, clientcmd.FlagAPIVersion+" for the cluster entry in .kubeconfig") cmd.Flags().Var(&options.insecureSkipTLSVerify, clientcmd.FlagInsecure, clientcmd.FlagInsecure+" for the cluster entry in .kubeconfig") - cmd.Flags().Var(&options.certificateAuthority, clientcmd.FlagCAFile, clientcmd.FlagCAFile+" for the cluster entry in .kubeconfig") + cmd.Flags().Var(&options.certificateAuthority, clientcmd.FlagCAFile, "path to "+clientcmd.FlagCAFile+" for the cluster entry in .kubeconfig") + cmd.Flags().Var(&options.embedCAData, clientcmd.FlagEmbedCerts, clientcmd.FlagEmbedCerts+" for the cluster entry in .kubeconfig") return cmd } @@ -116,11 +128,18 @@ func (o *createClusterOptions) modifyCluster(existingCluster clientcmdapi.Cluste } } if o.certificateAuthority.Provided() { - modifiedCluster.CertificateAuthority = o.certificateAuthority.Value() - // Specifying a certificate authority file clears certificate authority data and insecure mode - if modifiedCluster.CertificateAuthority != "" { - modifiedCluster.CertificateAuthorityData = nil + caPath := o.certificateAuthority.Value() + if o.embedCAData.Value() { + modifiedCluster.CertificateAuthorityData, _ = ioutil.ReadFile(caPath) modifiedCluster.InsecureSkipTLSVerify = false + modifiedCluster.CertificateAuthority = "" + } else { + modifiedCluster.CertificateAuthority = caPath + // Specifying a certificate authority file clears certificate authority data and insecure mode + if caPath != "" { + modifiedCluster.InsecureSkipTLSVerify = false + modifiedCluster.CertificateAuthorityData = nil + } } } @@ -145,6 +164,15 @@ func (o createClusterOptions) validate() error { if o.insecureSkipTLSVerify.Value() && o.certificateAuthority.Value() != "" { return errors.New("You cannot specify a certificate authority and insecure mode at the same time") } + if o.embedCAData.Value() { + caPath := o.certificateAuthority.Value() + if caPath == "" { + return fmt.Errorf("You must specify a --%s to embed", clientcmd.FlagCAFile) + } + if _, err := ioutil.ReadFile(caPath); err != nil { + return fmt.Errorf("Could not read %s data from %s: %v", clientcmd.FlagCAFile, caPath, err) + } + } return nil } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_context.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_context.go index aac3d588a26a..3869e28cf556 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_context.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/create_context.go @@ -36,18 +36,21 @@ type createContextOptions struct { namespace util.StringFlag } +const ( + create_context_long = `Sets a context entry in .kubeconfig +Specifying a name that already exists will merge new fields on top of existing values for those fields.` + create_context_example = `// Set the user field on the gce context entry without touching other values +$ kubectl config set-context gce --user=cluster-admin` +) + func NewCmdConfigSetContext(out io.Writer, pathOptions *pathOptions) *cobra.Command { options := &createContextOptions{pathOptions: pathOptions} cmd := &cobra.Command{ - Use: fmt.Sprintf("set-context name [--%v=cluster-nickname] [--%v=user-nickname] [--%v=namespace]", clientcmd.FlagClusterName, clientcmd.FlagAuthInfoName, clientcmd.FlagNamespace), - Short: "Sets a context entry in .kubeconfig", - Long: `Sets a context entry in .kubeconfig - Specifying a name that already exists will merge new fields on top of existing values for those fields. - e.g. - kubectl config set-context gce --user=cluster-admin - only sets the user field on the gce context entry without touching other values.`, - + Use: fmt.Sprintf("set-context NAME [--%v=cluster_nickname] [--%v=user_nickname] [--%v=namespace]", clientcmd.FlagClusterName, clientcmd.FlagAuthInfoName, clientcmd.FlagNamespace), + Short: "Sets a context entry in .kubeconfig", + Long: create_context_long, + Example: create_context_example, Run: func(cmd *cobra.Command, args []string) { if !options.complete(cmd) { return diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/set.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/set.go index ab11aaeb731e..b65a00f9d91a 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/set.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/set.go @@ -39,18 +39,17 @@ type setOptions struct { propertyValue string } +const set_long = `Sets an individual value in a .kubeconfig file +PROPERTY_NAME is a dot delimited name where each token represents either a attribute name or a map key. Map keys may not contain dots. +PROPERTY_VALUE is the new value you wish to set.` + func NewCmdConfigSet(out io.Writer, pathOptions *pathOptions) *cobra.Command { options := &setOptions{pathOptions: pathOptions} cmd := &cobra.Command{ - Use: "set property-name property-value", + Use: "set PROPERTY_NAME PROPERTY_VALUE", Short: "Sets an individual value in a .kubeconfig file", - Long: `Sets an individual value in a .kubeconfig file - - property-name is a dot delimited name where each token represents either a attribute name or a map key. Map keys may not contain dots. - property-value is the new value you wish to set. - - `, + Long: set_long, Run: func(cmd *cobra.Command, args []string) { if !options.complete(cmd) { return diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/unset.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/unset.go index d04a82a933e9..d0fda6a3e566 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/unset.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/unset.go @@ -32,16 +32,16 @@ type unsetOptions struct { propertyName string } +const unset_long = `Unsets an individual value in a .kubeconfig file +PROPERTY_NAME is a dot delimited name where each token represents either a attribute name or a map key. Map keys may not contain dots.` + func NewCmdConfigUnset(out io.Writer, pathOptions *pathOptions) *cobra.Command { options := &unsetOptions{pathOptions: pathOptions} cmd := &cobra.Command{ - Use: "unset property-name", + Use: "unset PROPERTY_NAME", Short: "Unsets an individual value in a .kubeconfig file", - Long: `Unsets an individual value in a .kubeconfig file - - property-name is a dot delimited name where each token represents either a attribute name or a map key. Map keys may not contain dots. - `, + Long: unset_long, Run: func(cmd *cobra.Command, args []string) { if !options.complete(cmd) { return diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/use_context.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/use_context.go index 1ac50fef1d30..0508fd1b7071 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/use_context.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/use_context.go @@ -35,7 +35,7 @@ func NewCmdConfigUseContext(out io.Writer, pathOptions *pathOptions) *cobra.Comm options := &useContextOptions{pathOptions: pathOptions} cmd := &cobra.Command{ - Use: "use-context context-name", + Use: "use-context CONTEXT_NAME", Short: "Sets the current-context in a .kubeconfig file", Long: `Sets the current-context in a .kubeconfig file`, Run: func(cmd *cobra.Command, args []string) { diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/view.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/view.go index 9bf8696623b0..18fc4afe79dc 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/view.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config/view.go @@ -25,8 +25,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api/latest" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -36,35 +34,39 @@ type viewOptions struct { merge util.BoolFlag } -func NewCmdConfigView(out io.Writer, pathOptions *pathOptions) *cobra.Command { - options := &viewOptions{pathOptions: pathOptions} +const ( + view_long = `displays merged .kubeconfig settings or a specified .kubeconfig file. - cmd := &cobra.Command{ - Use: "view", - Short: "displays merged .kubeconfig settings or a specified .kubeconfig file.", - Long: "displays merged .kubeconfig settings or a specified .kubeconfig file.", - Example: `// Show merged .kubeconfig settings. +You can use --output=template --template=TEMPLATE to extract specific values.` + view_example = `// Show merged .kubeconfig settings. $ kubectl config view // Show only local ./.kubeconfig settings -$ kubectl config view --local`, +$ kubectl config view --local + +// Get the password for the e2e user +$ kubectl config view -o template --template='{{ index . "users" "e2e" "password" }}'` +) + +func NewCmdConfigView(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &viewOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: "view", + Short: "displays merged .kubeconfig settings or a specified .kubeconfig file.", + Long: view_long, + Example: view_example, Run: func(cmd *cobra.Command, args []string) { options.complete() - printer, generic, err := cmdutil.PrinterForCommand(cmd) + printer, _, err := cmdutil.PrinterForCommand(cmd) if err != nil { glog.FatalDepth(1, err) } - if generic { - version := cmdutil.OutputVersion(cmd, latest.Version) - printer = kubectl.NewVersionedPrinter(printer, clientcmdapi.Scheme, version) - } - config, err := options.loadConfig() if err != nil { glog.FatalDepth(1, err) } - err = printer.PrintObj(config, out) if err != nil { glog.FatalDepth(1, err) diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/create.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/create.go index 187e6b0dc265..659f6ac31295 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/create.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/create.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" + cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -38,54 +39,67 @@ $ cat pod.json | kubectl create -f -` ) func (f *Factory) NewCmdCreate(out io.Writer) *cobra.Command { - flags := &struct { - Filenames util.StringList - }{} + var filenames util.StringList cmd := &cobra.Command{ - Use: "create -f filename", + Use: "create -f FILENAME", Short: "Create a resource by filename or stdin", Long: create_long, Example: create_example, Run: func(cmd *cobra.Command, args []string) { - schema, err := f.Validator(cmd) - checkErr(err) + err := RunCreate(f, out, cmd, filenames) + cmdutil.CheckErr(err) + }, + } + cmd.Flags().VarP(&filenames, "filename", "f", "Filename, directory, or URL to file to use to create the resource") + return cmd +} - cmdNamespace, err := f.DefaultNamespace(cmd) - checkErr(err) +func RunCreate(f *Factory, out io.Writer, cmd *cobra.Command, filenames util.StringList) error { + schema, err := f.Validator() + if err != nil { + return err + } - mapper, typer := f.Object(cmd) - r := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand(cmd)). - ContinueOnError(). - NamespaceParam(cmdNamespace).RequireNamespace(). - FilenameParam(flags.Filenames...). - Flatten(). - Do() - checkErr(r.Err()) + cmdNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } - count := 0 - err = r.Visit(func(info *resource.Info) error { - data, err := info.Mapping.Codec.Encode(info.Object) - if err != nil { - return err - } - if err := schema.ValidateBytes(data); err != nil { - return err - } - obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, data) - if err != nil { - return err - } - count++ - info.Refresh(obj, true) - fmt.Fprintf(out, "%s\n", info.Name) - return nil - }) - checkErr(err) - if count == 0 { - checkErr(fmt.Errorf("no objects passed to create")) - } - }, + mapper, typer := f.Object() + r := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand(cmd)). + ContinueOnError(). + NamespaceParam(cmdNamespace).RequireNamespace(). + FilenameParam(filenames...). + Flatten(). + Do() + err = r.Err() + if err != nil { + return err } - cmd.Flags().VarP(&flags.Filenames, "filename", "f", "Filename, directory, or URL to file to use to create the resource") - return cmd + + count := 0 + err = r.Visit(func(info *resource.Info) error { + data, err := info.Mapping.Codec.Encode(info.Object) + if err != nil { + return err + } + if err := schema.ValidateBytes(data); err != nil { + return err + } + obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, data) + if err != nil { + return err + } + count++ + info.Refresh(obj, true) + fmt.Fprintf(out, "%s\n", info.Name) + return nil + }) + if err != nil { + return err + } + if count == 0 { + return fmt.Errorf("no objects passed to create") + } + return nil } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/create_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/create_test.go index 7f00d78268d2..ebd1d00cdefd 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/create_test.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/create_test.go @@ -25,8 +25,8 @@ import ( ) func TestCreateObject(t *testing.T) { - pods, _ := testData() - pods.Items[0].Name = "redis-master" + _, _, rc := testData() + rc.Items[0].Name = "redis-master-controller" f, tf, codec := NewAPIFactory() tf.Printer = &testPrinter{} @@ -34,8 +34,8 @@ func TestCreateObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/pods" && m == "POST": - return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -46,17 +46,17 @@ func TestCreateObject(t *testing.T) { buf := bytes.NewBuffer([]byte{}) cmd := f.NewCmdCreate(buf) - cmd.Flags().Set("filename", "../../../examples/guestbook/redis-master.json") + cmd.Flags().Set("filename", "../../../examples/guestbook/redis-master-controller.json") cmd.Run(cmd, []string{}) // uses the name from the file, not the response - if buf.String() != "redis-master\n" { + if buf.String() != "redis-master-controller\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestCreateMultipleObject(t *testing.T) { - pods, svc := testData() + _, svc, rc := testData() f, tf, codec := NewAPIFactory() tf.Printer = &testPrinter{} @@ -64,10 +64,10 @@ func TestCreateMultipleObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/pods" && m == "POST": - return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil case p == "/namespaces/test/services" && m == "POST": return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -78,19 +78,19 @@ func TestCreateMultipleObject(t *testing.T) { buf := bytes.NewBuffer([]byte{}) cmd := f.NewCmdCreate(buf) - cmd.Flags().Set("filename", "../../../examples/guestbook/redis-master.json") + cmd.Flags().Set("filename", "../../../examples/guestbook/redis-master-controller.json") cmd.Flags().Set("filename", "../../../examples/guestbook/frontend-service.json") cmd.Run(cmd, []string{}) // Names should come from the REST response, NOT the files - if buf.String() != "foo\nbaz\n" { + if buf.String() != "rc1\nbaz\n" { t.Errorf("unexpected output: %s", buf.String()) } } func TestCreateDirectory(t *testing.T) { - pods, svc := testData() - pods.Items[0].Name = "redis-master" + _, svc, rc := testData() + rc.Items[0].Name = "name" f, tf, codec := NewAPIFactory() tf.Printer = &testPrinter{} @@ -98,12 +98,10 @@ func TestCreateDirectory(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/pods" && m == "POST": - return &http.Response{StatusCode: 201, Body: objBody(codec, &pods.Items[0])}, nil case p == "/namespaces/test/services" && m == "POST": return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil case p == "/namespaces/test/replicationcontrollers" && m == "POST": - return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil + return &http.Response{StatusCode: 201, Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -117,7 +115,7 @@ func TestCreateDirectory(t *testing.T) { cmd.Flags().Set("filename", "../../../examples/guestbook") cmd.Run(cmd, []string{}) - if buf.String() != "baz\nbaz\nbaz\nredis-master\nbaz\nbaz\n" { + if buf.String() != "name\nbaz\nname\nbaz\nname\nbaz\n" { t.Errorf("unexpected output: %s", buf.String()) } } diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/delete.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/delete.go index 8797ab24648f..c3bbe58d58f4 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/delete.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/delete.go @@ -56,46 +56,57 @@ $ kubectl delete pods --all` ) func (f *Factory) NewCmdDelete(out io.Writer) *cobra.Command { - flags := &struct { - Filenames util.StringList - }{} + var filenames util.StringList cmd := &cobra.Command{ - Use: "delete ([-f filename] | ( [( | -l