Merge pull request #40927 from soltysh/deployment_logs

Automatic merge from submit-queue (batch tested with PRs 42053, 41282, 42056, 41663, 40927)

Allow getting logs directly from deployment, job and statefulset

**Special notes for your reviewer**:
@smarterclayton you asked for it in OpenShift


```release-note
kubectl logs allows getting logs directly from deployment, job and statefulset
```
This commit is contained in:
Kubernetes Submit Queue
2017-02-27 12:45:36 -08:00
committed by GitHub
15 changed files with 289 additions and 74 deletions

View File

@@ -438,7 +438,7 @@ func getRESTMappings(mapper meta.RESTMapper, pruneResources *[]pruneResource) (n
type pruner struct {
mapper meta.RESTMapper
clientFunc resource.ClientMapperFunc
clientsetFunc func() (*internalclientset.Clientset, error)
clientsetFunc func() (internalclientset.Interface, error)
visitedUids sets.String
selector labels.Selector
@@ -500,7 +500,7 @@ func (p *pruner) delete(namespace, name string, mapping *meta.RESTMapping, c res
return runDelete(namespace, name, mapping, c, nil, p.cascade, p.gracePeriod, p.clientsetFunc)
}
func runDelete(namespace, name string, mapping *meta.RESTMapping, c resource.RESTClient, helper *resource.Helper, cascade bool, gracePeriod int, clientsetFunc func() (*internalclientset.Clientset, error)) error {
func runDelete(namespace, name string, mapping *meta.RESTMapping, c resource.RESTClient, helper *resource.Helper, cascade bool, gracePeriod int, clientsetFunc func() (internalclientset.Interface, error)) error {
if !cascade {
if helper == nil {
helper = resource.NewHelper(c, mapping)
@@ -538,7 +538,7 @@ type patcher struct {
mapping *meta.RESTMapping
helper *resource.Helper
clientsetFunc func() (*internalclientset.Clientset, error)
clientsetFunc func() (internalclientset.Interface, error)
overwrite bool
backOff clockwork.Clock

View File

@@ -47,7 +47,7 @@ import (
)
type DrainOptions struct {
client *internalclientset.Clientset
client internalclientset.Interface
restClient *restclient.RESTClient
factory cmdutil.Factory
Force bool
@@ -583,7 +583,7 @@ func (o *DrainOptions) waitForDelete(pods []api.Pod, interval, timeout time.Dura
// SupportEviction uses Discovery API to find out if the server support eviction subresource
// If support, it will return its groupVersion; Otherwise, it will return ""
func SupportEviction(clientset *internalclientset.Clientset) (string, error) {
func SupportEviction(clientset internalclientset.Interface) (string, error) {
discoveryClient := clientset.Discovery()
groupList, err := discoveryClient.ServerGroups()
if err != nil {

View File

@@ -54,13 +54,19 @@ var (
kubectl logs --tail=20 nginx
# Show all logs from pod nginx written in the last hour
kubectl logs --since=1h nginx`)
kubectl logs --since=1h nginx
# Return snapshot logs from first container of a job named hello
kubectl logs job/hello
# Return snapshot logs from container nginx-1 of a deployment named nginx
kubectl logs deployment/nginx -c nginx-1`)
selectorTail int64 = 10
)
const (
logsUsageStr = "expected 'logs POD_NAME [CONTAINER_NAME]'.\nPOD_NAME is a required argument for the logs command"
logsUsageStr = "expected 'logs (POD | TYPE/NAME) [CONTAINER_NAME]'.\nPOD or TYPE/NAME is a required argument for the logs command"
)
type LogsOptions struct {
@@ -83,9 +89,9 @@ type LogsOptions struct {
func NewCmdLogs(f cmdutil.Factory, out io.Writer) *cobra.Command {
o := &LogsOptions{}
cmd := &cobra.Command{
Use: "logs [-f] [-p] POD [-c CONTAINER]",
Use: "logs [-f] [-p] (POD | TYPE/NAME) [-c CONTAINER]",
Short: i18n.T("Print the logs for a container in a pod"),
Long: "Print the logs for a container in a pod. If the pod has only one container, the container name is optional.",
Long: "Print the logs for a container in a pod or specified resource. If the pod has only one container, the container name is optional.",
Example: logs_example,
PreRun: func(cmd *cobra.Command, args []string) {
if len(os.Args) > 1 && os.Args[1] == "log" {
@@ -94,9 +100,7 @@ func NewCmdLogs(f cmdutil.Factory, out io.Writer) *cobra.Command {
},
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(o.Complete(f, out, cmd, args))
if err := o.Validate(); err != nil {
cmdutil.CheckErr(cmdutil.UsageError(cmd, err.Error()))
}
cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.RunLogs())
},
Aliases: []string{"log"},

View File

@@ -245,7 +245,7 @@ func RunRollingUpdate(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args
// than the old rc. This selector is the hash of the rc, with a suffix to provide uniqueness for
// same-image updates.
if len(image) != 0 {
codec := api.Codecs.LegacyCodec(clientset.CoreClient.RESTClient().APIVersion())
codec := api.Codecs.LegacyCodec(v1.SchemeGroupVersion)
keepOldName = len(args) == 1
newName := findNewName(args, oldRc)
if newRc, err = kubectl.LoadExistingNextReplicationController(coreClient, cmdNamespace, newName); err != nil {

View File

@@ -286,7 +286,7 @@ func (f *FakeFactory) RESTClient() (*restclient.RESTClient, error) {
return nil, nil
}
func (f *FakeFactory) ClientSet() (*internalclientset.Clientset, error) {
func (f *FakeFactory) ClientSet() (internalclientset.Interface, error) {
return nil, nil
}
@@ -311,7 +311,7 @@ func (f *FakeFactory) FederationClientSetForVersion(version *schema.GroupVersion
func (f *FakeFactory) FederationClientForVersion(version *schema.GroupVersion) (*restclient.RESTClient, error) {
return nil, nil
}
func (f *FakeFactory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (*internalclientset.Clientset, error) {
func (f *FakeFactory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error) {
return nil, nil
}
func (f *FakeFactory) ClientConfigForVersion(requiredVersion *schema.GroupVersion) (*restclient.Config, error) {
@@ -536,7 +536,7 @@ func (f *fakeAPIFactory) JSONEncoder() runtime.Encoder {
return testapi.Default.Codec()
}
func (f *fakeAPIFactory) ClientSet() (*internalclientset.Clientset, error) {
func (f *fakeAPIFactory) ClientSet() (internalclientset.Interface, error) {
// Swap the HTTP client out of the REST client with the fake
// version.
fakeClient := f.tf.Client.(*fake.RESTClient)

View File

@@ -75,6 +75,7 @@ go_test(
name = "go_default_test",
srcs = [
"cached_discovery_test.go",
"factory_object_mapping_test.go",
"factory_test.go",
"helpers_test.go",
"shortcut_restmapper_test.go",
@@ -90,7 +91,10 @@ go_test(
"//pkg/api/testing:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/api/validation:go_default_library",
"//pkg/apis/apps:go_default_library",
"//pkg/apis/batch:go_default_library",
"//pkg/apis/extensions:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/kubectl:go_default_library",
@@ -104,6 +108,7 @@ go_test(
"//vendor:k8s.io/apimachinery/pkg/labels",
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
"//vendor:k8s.io/apimachinery/pkg/util/diff",
"//vendor:k8s.io/apimachinery/pkg/util/validation/field",
"//vendor:k8s.io/apimachinery/pkg/version",
"//vendor:k8s.io/apimachinery/pkg/watch",

View File

@@ -32,7 +32,7 @@ import (
func NewClientCache(loader clientcmd.ClientConfig, discoveryClientFactory DiscoveryClientFactory) *ClientCache {
return &ClientCache{
clientsets: make(map[schema.GroupVersion]*internalclientset.Clientset),
clientsets: make(map[schema.GroupVersion]internalclientset.Interface),
configs: make(map[schema.GroupVersion]*restclient.Config),
fedClientSets: make(map[schema.GroupVersion]fedclientset.Interface),
loader: loader,
@@ -44,7 +44,7 @@ func NewClientCache(loader clientcmd.ClientConfig, discoveryClientFactory Discov
// is invoked only once
type ClientCache struct {
loader clientcmd.ClientConfig
clientsets map[schema.GroupVersion]*internalclientset.Clientset
clientsets map[schema.GroupVersion]internalclientset.Interface
fedClientSets map[schema.GroupVersion]fedclientset.Interface
configs map[schema.GroupVersion]*restclient.Config
@@ -144,7 +144,7 @@ func copyConfig(in *restclient.Config) *restclient.Config {
// ClientSetForVersion initializes or reuses a clientset for the specified version, or returns an
// error if that is not possible
func (c *ClientCache) ClientSetForVersion(requiredVersion *schema.GroupVersion) (*internalclientset.Clientset, error) {
func (c *ClientCache) ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error) {
if requiredVersion != nil {
if clientset, ok := c.clientsets[*requiredVersion]; ok {
return clientset, nil

View File

@@ -86,7 +86,7 @@ type ClientAccessFactory interface {
DiscoveryClientFactory
// ClientSet gives you back an internal, generated clientset
ClientSet() (*internalclientset.Clientset, error)
ClientSet() (internalclientset.Interface, error)
// Returns a RESTClient for accessing Kubernetes resources or an error.
RESTClient() (*restclient.RESTClient, error)
// Returns a client.Config for accessing the Kubernetes server.
@@ -101,7 +101,7 @@ type ClientAccessFactory interface {
// TODO remove this should be rolled into restclient with the right version
FederationClientForVersion(version *schema.GroupVersion) (*restclient.RESTClient, error)
// TODO remove. This should be rolled into `ClientSet`
ClientSetForVersion(requiredVersion *schema.GroupVersion) (*internalclientset.Clientset, error)
ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error)
// TODO remove. This should be rolled into `ClientConfig`
ClientConfigForVersion(requiredVersion *schema.GroupVersion) (*restclient.Config, error)

View File

@@ -168,11 +168,11 @@ func (f *ring0Factory) DiscoveryClient() (discovery.CachedDiscoveryInterface, er
return f.discoveryFactory.DiscoveryClient()
}
func (f *ring0Factory) ClientSet() (*internalclientset.Clientset, error) {
func (f *ring0Factory) ClientSet() (internalclientset.Interface, error) {
return f.clientCache.ClientSetForVersion(nil)
}
func (f *ring0Factory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (*internalclientset.Clientset, error) {
func (f *ring0Factory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error) {
return f.clientCache.ClientSetForVersion(requiredVersion)
}

View File

@@ -213,51 +213,48 @@ func (f *ring1Factory) LogsForObject(object, options runtime.Object) (*restclien
if err != nil {
return nil, err
}
opts, ok := options.(*api.PodLogOptions)
if !ok {
return nil, errors.New("provided options object is not a PodLogOptions")
}
var selector labels.Selector
var namespace string
switch t := object.(type) {
case *api.Pod:
opts, ok := options.(*api.PodLogOptions)
if !ok {
return nil, errors.New("provided options object is not a PodLogOptions")
}
return clientset.Core().Pods(t.Namespace).GetLogs(t.Name, opts), nil
case *api.ReplicationController:
opts, ok := options.(*api.PodLogOptions)
if !ok {
return nil, errors.New("provided options object is not a PodLogOptions")
}
selector := labels.SelectorFromSet(t.Spec.Selector)
sortBy := func(pods []*v1.Pod) sort.Interface { return controller.ByLogging(pods) }
pod, numPods, err := GetFirstPod(clientset.Core(), t.Namespace, selector, 20*time.Second, sortBy)
if err != nil {
return nil, err
}
if numPods > 1 {
fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name)
}
return clientset.Core().Pods(pod.Namespace).GetLogs(pod.Name, opts), nil
namespace = t.Namespace
selector = labels.SelectorFromSet(t.Spec.Selector)
case *extensions.ReplicaSet:
opts, ok := options.(*api.PodLogOptions)
if !ok {
return nil, errors.New("provided options object is not a PodLogOptions")
}
selector, err := metav1.LabelSelectorAsSelector(t.Spec.Selector)
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return nil, fmt.Errorf("invalid label selector: %v", err)
}
sortBy := func(pods []*v1.Pod) sort.Interface { return controller.ByLogging(pods) }
pod, numPods, err := GetFirstPod(clientset.Core(), t.Namespace, selector, 20*time.Second, sortBy)
case *extensions.Deployment:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return nil, err
}
if numPods > 1 {
fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name)
return nil, fmt.Errorf("invalid label selector: %v", err)
}
return clientset.Core().Pods(pod.Namespace).GetLogs(pod.Name, opts), nil
case *batch.Job:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return nil, fmt.Errorf("invalid label selector: %v", err)
}
case *apps.StatefulSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return nil, fmt.Errorf("invalid label selector: %v", err)
}
default:
gvks, _, err := api.Scheme.ObjectKinds(object)
@@ -266,6 +263,16 @@ func (f *ring1Factory) LogsForObject(object, options runtime.Object) (*restclien
}
return nil, fmt.Errorf("cannot get the logs from %v", gvks[0])
}
sortBy := func(pods []*v1.Pod) sort.Interface { return controller.ByLogging(pods) }
pod, numPods, err := GetFirstPod(clientset.Core(), namespace, selector, 20*time.Second, sortBy)
if err != nil {
return nil, err
}
if numPods > 1 {
fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name)
}
return clientset.Core().Pods(pod.Namespace).GetLogs(pod.Name, opts), nil
}
func (f *ring1Factory) Scaler(mapping *meta.RESTMapping) (kubectl.Scaler, error) {
@@ -329,29 +336,35 @@ func (f *ring1Factory) AttachablePodForObject(object runtime.Object) (*api.Pod,
case *extensions.ReplicaSet:
namespace = t.Namespace
selector = labels.SelectorFromSet(t.Spec.Selector.MatchLabels)
case *api.ReplicationController:
namespace = t.Namespace
selector = labels.SelectorFromSet(t.Spec.Selector)
case *apps.StatefulSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return nil, fmt.Errorf("invalid label selector: %v", err)
}
case *extensions.Deployment:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return nil, fmt.Errorf("invalid label selector: %v", err)
}
case *batch.Job:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return nil, fmt.Errorf("invalid label selector: %v", err)
}
case *api.Pod:
return t, nil
default:
gvks, _, err := api.Scheme.ObjectKinds(object)
if err != nil {
@@ -359,6 +372,7 @@ func (f *ring1Factory) AttachablePodForObject(object runtime.Object) (*api.Pod,
}
return nil, fmt.Errorf("cannot attach to %v: not implemented", gvks[0])
}
sortBy := func(pods []*v1.Pod) sort.Interface { return sort.Reverse(controller.ActivePods(pods)) }
pod, _, err := GetFirstPod(clientset.Core(), namespace, selector, 1*time.Minute, sortBy)
return pod, err

View File

@@ -0,0 +1,192 @@
/*
Copyright 2017 The Kubernetes Authors.
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 util
import (
"reflect"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff"
testclient "k8s.io/client-go/testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/apps"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
)
type fakeClientAccessFactory struct {
ClientAccessFactory
fakeClientset *fake.Clientset
}
func (f *fakeClientAccessFactory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error) {
return f.fakeClientset, nil
}
func newFakeClientAccessFactory(objs []runtime.Object) *fakeClientAccessFactory {
return &fakeClientAccessFactory{
fakeClientset: fake.NewSimpleClientset(objs...),
}
}
var (
podsResource = schema.GroupVersionResource{Resource: "pods"}
)
func TestLogsForObject(t *testing.T) {
tests := []struct {
name string
obj runtime.Object
opts *api.PodLogOptions
pods []runtime.Object
actions []testclient.Action
}{
{
name: "pod logs",
obj: &api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"},
},
pods: []runtime.Object{testPod()},
actions: []testclient.Action{
getLogsAction("test", nil),
},
},
{
name: "replication controller logs",
obj: &api.ReplicationController{
ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"},
Spec: api.ReplicationControllerSpec{
Selector: map[string]string{"foo": "bar"},
},
},
pods: []runtime.Object{testPod()},
actions: []testclient.Action{
testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}),
getLogsAction("test", nil),
},
},
{
name: "replica set logs",
obj: &extensions.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"},
Spec: extensions.ReplicaSetSpec{
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}},
},
},
pods: []runtime.Object{testPod()},
actions: []testclient.Action{
testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}),
getLogsAction("test", nil),
},
},
{
name: "deployment logs",
obj: &extensions.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"},
Spec: extensions.DeploymentSpec{
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}},
},
},
pods: []runtime.Object{testPod()},
actions: []testclient.Action{
testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}),
getLogsAction("test", nil),
},
},
{
name: "job logs",
obj: &batch.Job{
ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"},
Spec: batch.JobSpec{
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}},
},
},
pods: []runtime.Object{testPod()},
actions: []testclient.Action{
testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}),
getLogsAction("test", nil),
},
},
{
name: "stateful set logs",
obj: &apps.StatefulSet{
ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"},
Spec: apps.StatefulSetSpec{
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}},
},
},
pods: []runtime.Object{testPod()},
actions: []testclient.Action{
testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}),
getLogsAction("test", nil),
},
},
}
for _, test := range tests {
caf := newFakeClientAccessFactory(test.pods)
omf := NewObjectMappingFactory(caf)
_, err := omf.LogsForObject(test.obj, test.opts)
if err != nil {
t.Errorf("%s: unexpected error: %v", test.name, err)
continue
}
for i := range test.actions {
if len(caf.fakeClientset.Actions()) < i {
t.Errorf("%s: action %d does not exists in actual actions: %#v",
test.name, i, caf.fakeClientset.Actions())
continue
}
got := caf.fakeClientset.Actions()[i]
want := test.actions[i]
if !reflect.DeepEqual(got, want) {
t.Errorf("%s: unexpected action: %s", test.name, diff.ObjectDiff(got, want))
}
}
}
}
func testPod() runtime.Object {
return &api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "test",
Labels: map[string]string{"foo": "bar"},
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "c1"}},
},
}
}
func getLogsAction(namespace string, opts *api.PodLogOptions) testclient.Action {
action := testclient.GenericActionImpl{}
action.Verb = "get"
action.Namespace = namespace
action.Resource = podsResource
action.Subresource = "logs"
action.Value = opts
return action
}