Pod log subresource

Adds a Log subresource to Pod storage. The Log subresource implements
rest.GetterWithOptions and produces a ResourceStreamer resource that
will stream the log output from the pod's host node.
This commit is contained in:
Cesar Wong
2015-04-06 14:57:06 -04:00
parent efc7f86baf
commit 8df4758ee9
8 changed files with 319 additions and 25 deletions

View File

@@ -25,10 +25,12 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
etcderr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic"
etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd"
genericrest "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
@@ -40,6 +42,7 @@ type PodStorage struct {
Pod *REST
Binding *BindingREST
Status *StatusREST
Log *LogREST
}
// REST implements a RESTStorage for pods against etcd
@@ -48,7 +51,7 @@ type REST struct {
}
// NewStorage returns a RESTStorage object that will work against pods.
func NewStorage(h tools.EtcdHelper) PodStorage {
func NewStorage(h tools.EtcdHelper, k client.ConnectionInfoGetter) PodStorage {
prefix := "/registry/pods"
store := &etcdgeneric.Etcd{
NewFunc: func() runtime.Object { return &api.Pod{} },
@@ -85,6 +88,7 @@ func NewStorage(h tools.EtcdHelper) PodStorage {
Pod: &REST{*store},
Binding: &BindingREST{store: store},
Status: &StatusREST{store: &statusStore},
Log: &LogREST{store: store, kubeletConn: k},
}
}
@@ -186,3 +190,37 @@ func (r *StatusREST) New() runtime.Object {
func (r *StatusREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) {
return r.store.Update(ctx, obj)
}
// LogREST implements the log endpoint for a Pod
type LogREST struct {
store *etcdgeneric.Etcd
kubeletConn client.ConnectionInfoGetter
}
// New creates a new Pod log options object
func (r *LogREST) New() runtime.Object {
return &api.PodLogOptions{}
}
// Get retrieves a runtime.Object that will stream the contents of the pod log
func (r *LogREST) Get(ctx api.Context, name string, opts runtime.Object) (runtime.Object, error) {
logOpts, ok := opts.(*api.PodLogOptions)
if !ok {
return nil, fmt.Errorf("Invalid options object: %#v", opts)
}
location, transport, err := pod.LogLocation(r.store, r.kubeletConn, ctx, name, logOpts)
if err != nil {
return nil, err
}
return &genericrest.LocationStreamer{
Location: location,
Transport: transport,
ContentType: "text/plain",
Flush: logOpts.Follow,
}, nil
}
// NewGetOptions creates a new options object
func (r *LogREST) NewGetOptions() runtime.Object {
return &api.PodLogOptions{}
}

View File

@@ -47,7 +47,7 @@ func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) {
func newStorage(t *testing.T) (*REST, *BindingREST, *StatusREST, *tools.FakeEtcdClient, tools.EtcdHelper) {
fakeEtcdClient, h := newHelper(t)
storage := NewStorage(h)
storage := NewStorage(h, nil)
return storage.Pod, storage.Binding, storage.Status, fakeEtcdClient, h
}
@@ -89,7 +89,7 @@ func TestStorage(t *testing.T) {
func TestCreate(t *testing.T) {
fakeEtcdClient, helper := newHelper(t)
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
test := resttest.New(t, storage, fakeEtcdClient.SetError)
pod := validNewPod()
pod.ObjectMeta = api.ObjectMeta{}
@@ -107,7 +107,7 @@ func TestCreate(t *testing.T) {
func TestDelete(t *testing.T) {
fakeEtcdClient, helper := newHelper(t)
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
test := resttest.New(t, storage, fakeEtcdClient.SetError)
createFn := func() runtime.Object {
@@ -143,7 +143,7 @@ func expectPod(t *testing.T, out runtime.Object) (*api.Pod, bool) {
func TestCreateRegistryError(t *testing.T) {
fakeEtcdClient, helper := newHelper(t)
fakeEtcdClient.Err = fmt.Errorf("test error")
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
pod := validNewPod()
_, err := storage.Create(api.NewDefaultContext(), pod)
@@ -154,7 +154,7 @@ func TestCreateRegistryError(t *testing.T) {
func TestCreateSetsFields(t *testing.T) {
fakeEtcdClient, helper := newHelper(t)
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
pod := validNewPod()
_, err := storage.Create(api.NewDefaultContext(), pod)
if err != fakeEtcdClient.Err {
@@ -176,7 +176,7 @@ func TestCreateSetsFields(t *testing.T) {
func TestListError(t *testing.T) {
fakeEtcdClient, helper := newHelper(t)
fakeEtcdClient.Err = fmt.Errorf("test error")
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
pods, err := storage.List(api.NewDefaultContext(), labels.Everything(), fields.Everything())
if err != fakeEtcdClient.Err {
t.Fatalf("Expected %#v, Got %#v", fakeEtcdClient.Err, err)
@@ -194,7 +194,7 @@ func TestListEmptyPodList(t *testing.T) {
E: fakeEtcdClient.NewError(tools.EtcdErrorCodeNotFound),
}
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
pods, err := storage.List(api.NewContext(), labels.Everything(), fields.Everything())
if err != nil {
t.Fatalf("unexpected error: %v", err)
@@ -231,7 +231,7 @@ func TestListPodList(t *testing.T) {
},
},
}
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
podsObj, err := storage.List(api.NewDefaultContext(), labels.Everything(), fields.Everything())
pods := podsObj.(*api.PodList)
@@ -280,7 +280,7 @@ func TestListPodListSelection(t *testing.T) {
},
},
}
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
ctx := api.NewDefaultContext()
@@ -345,7 +345,7 @@ func TestListPodListSelection(t *testing.T) {
}
func TestPodDecode(t *testing.T) {
storage := NewStorage(tools.EtcdHelper{}).Pod
storage := NewStorage(tools.EtcdHelper{}, nil).Pod
expected := validNewPod()
body, err := latest.Codec.Encode(expected)
if err != nil {
@@ -375,7 +375,7 @@ func TestGet(t *testing.T) {
},
},
}
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
obj, err := storage.Get(api.WithNamespace(api.NewContext(), "test"), "foo")
pod := obj.(*api.Pod)
@@ -392,7 +392,7 @@ func TestGet(t *testing.T) {
func TestPodStorageValidatesCreate(t *testing.T) {
fakeEtcdClient, helper := newHelper(t)
fakeEtcdClient.Err = fmt.Errorf("test error")
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
pod := validNewPod()
pod.Labels = map[string]string{
@@ -410,7 +410,7 @@ func TestPodStorageValidatesCreate(t *testing.T) {
// TODO: remove, this is covered by RESTTest.TestCreate
func TestCreatePod(t *testing.T) {
_, helper := newHelper(t)
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
pod := validNewPod()
obj, err := storage.Create(api.NewDefaultContext(), pod)
@@ -432,7 +432,7 @@ func TestCreatePod(t *testing.T) {
// TODO: remove, this is covered by RESTTest.TestCreate
func TestCreateWithConflictingNamespace(t *testing.T) {
_, helper := newHelper(t)
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
pod := validNewPod()
pod.Namespace = "not-default"
@@ -461,7 +461,7 @@ func TestUpdateWithConflictingNamespace(t *testing.T) {
},
},
}
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
pod := validChangedPod()
pod.Namespace = "not-default"
@@ -578,7 +578,7 @@ func TestResourceLocation(t *testing.T) {
},
},
}
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
redirector := rest.Redirector(storage)
location, _, err := redirector.ResourceLocation(api.NewDefaultContext(), tc.query)
@@ -616,7 +616,7 @@ func TestDeletePod(t *testing.T) {
},
},
}
storage := NewStorage(helper).Pod
storage := NewStorage(helper, nil).Pod
_, err := storage.Delete(api.NewDefaultContext(), "foo", nil)
if err != nil {

View File

@@ -26,6 +26,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"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/registry/generic"
@@ -133,6 +134,18 @@ type ResourceGetter interface {
Get(api.Context, string) (runtime.Object, error)
}
func getPod(getter ResourceGetter, ctx api.Context, name string) (*api.Pod, error) {
obj, err := getter.Get(ctx, name)
if err != nil {
return nil, err
}
pod := obj.(*api.Pod)
if pod == nil {
return nil, fmt.Errorf("Unexpected object type: %#v", pod)
}
return pod, nil
}
// ResourceLocation returns a URL to which one can send traffic for the specified pod.
func ResourceLocation(getter ResourceGetter, ctx api.Context, id string) (*url.URL, http.RoundTripper, error) {
// Allow ID as "podname" or "podname:port". If port is not specified,
@@ -148,14 +161,10 @@ func ResourceLocation(getter ResourceGetter, ctx api.Context, id string) (*url.U
port = parts[1]
}
obj, err := getter.Get(ctx, name)
pod, err := getPod(getter, ctx, name)
if err != nil {
return nil, nil, err
}
pod := obj.(*api.Pod)
if pod == nil {
return nil, nil, nil
}
// Try to figure out a port.
if port == "" {
@@ -177,3 +186,43 @@ func ResourceLocation(getter ResourceGetter, ctx api.Context, id string) (*url.U
}
return loc, nil, nil
}
// LogLocation returns a the log URL for a pod container. If opts.Container is blank
// and only one container is present in the pod, that container is used.
func LogLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, ctx api.Context, name string, opts *api.PodLogOptions) (*url.URL, http.RoundTripper, error) {
pod, err := getPod(getter, ctx, name)
if err != nil {
return nil, nil, err
}
// Try to figure out a container
container := opts.Container
if container == "" {
if len(pod.Spec.Containers) == 1 {
container = pod.Spec.Containers[0].Name
} else {
return nil, nil, fmt.Errorf("a container name must be specified for pod %s", name)
}
}
nodeHost := pod.Status.HostIP
if len(nodeHost) == 0 {
// If pod has not been assigned a host, return an empty location
return nil, nil, nil
}
nodeScheme, nodePort, nodeTransport, err := connInfo.GetConnectionInfo(nodeHost)
if err != nil {
return nil, nil, err
}
params := url.Values{}
if opts.Follow {
params.Add("follow", "true")
}
loc := &url.URL{
Scheme: nodeScheme,
Host: fmt.Sprintf("%s:%d", nodeHost, nodePort),
Path: fmt.Sprintf("/containerLogs/%s/%s/%s", pod.Namespace, name, container),
RawQuery: params.Encode(),
}
return loc, nodeTransport, nil
}