From 1b46bc010ae41bcccdaa1bca3bea432f989d2857 Mon Sep 17 00:00:00 2001 From: Maciej Szulik Date: Fri, 21 Aug 2015 16:23:12 +0200 Subject: [PATCH] Job clients, printer and describer --- contrib/completions/bash/kubectl | 3 + pkg/client/unversioned/experimental.go | 5 + pkg/client/unversioned/jobs.go | 112 +++++++++ pkg/client/unversioned/jobs_test.go | 222 ++++++++++++++++++ .../unversioned/testclient/testclient.go | 4 + pkg/kubectl/describe.go | 42 ++++ pkg/kubectl/resource_printer.go | 31 ++- pkg/registry/job/doc.go | 2 +- pkg/registry/job/etcd/etcd.go | 2 +- 9 files changed, 420 insertions(+), 3 deletions(-) create mode 100644 pkg/client/unversioned/jobs.go create mode 100644 pkg/client/unversioned/jobs_test.go diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index f78bc485e80..a8c9c79e4e7 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -291,6 +291,7 @@ _kubectl_get() must_have_one_noun+=("endpoints") must_have_one_noun+=("event") must_have_one_noun+=("horizontalpodautoscaler") + must_have_one_noun+=("job") must_have_one_noun+=("limitrange") must_have_one_noun+=("namespace") must_have_one_noun+=("node") @@ -459,6 +460,7 @@ _kubectl_delete() must_have_one_noun+=("endpoints") must_have_one_noun+=("event") must_have_one_noun+=("horizontalpodautoscaler") + must_have_one_noun+=("job") must_have_one_noun+=("limitrange") must_have_one_noun+=("namespace") must_have_one_noun+=("node") @@ -829,6 +831,7 @@ _kubectl_label() must_have_one_noun+=("endpoints") must_have_one_noun+=("event") must_have_one_noun+=("horizontalpodautoscaler") + must_have_one_noun+=("job") must_have_one_noun+=("limitrange") must_have_one_noun+=("namespace") must_have_one_noun+=("node") diff --git a/pkg/client/unversioned/experimental.go b/pkg/client/unversioned/experimental.go index 68b8ca90b80..d30f0b00a50 100644 --- a/pkg/client/unversioned/experimental.go +++ b/pkg/client/unversioned/experimental.go @@ -36,6 +36,7 @@ type ExperimentalInterface interface { ScaleNamespacer DaemonSetsNamespacer DeploymentsNamespacer + JobsNamespacer } // ExperimentalClient is used to interact with experimental Kubernetes features. @@ -90,6 +91,10 @@ func (c *ExperimentalClient) Deployments(namespace string) DeploymentInterface { return newDeployments(c, namespace) } +func (c *ExperimentalClient) Jobs(namespace string) JobInterface { + return newJobs(c, namespace) +} + // NewExperimental creates a new ExperimentalClient for the given config. This client // provides access to experimental Kubernetes features. // Experimental features are not supported and may be changed or removed in diff --git a/pkg/client/unversioned/jobs.go b/pkg/client/unversioned/jobs.go new file mode 100644 index 00000000000..20a90f00f84 --- /dev/null +++ b/pkg/client/unversioned/jobs.go @@ -0,0 +1,112 @@ +/* +Copyright 2015 The Kubernetes Authors 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 unversioned + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/experimental" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/watch" +) + +// JobsNamespacer has methods to work with Job resources in a namespace +type JobsNamespacer interface { + Jobs(namespace string) JobInterface +} + +// JobInterface exposes methods to work on Job resources. +type JobInterface interface { + List(label labels.Selector, field fields.Selector) (*experimental.JobList, error) + Get(name string) (*experimental.Job, error) + Create(job *experimental.Job) (*experimental.Job, error) + Update(job *experimental.Job) (*experimental.Job, error) + Delete(name string, options *api.DeleteOptions) error + Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + UpdateStatus(job *experimental.Job) (*experimental.Job, error) +} + +// jobs implements JobsNamespacer interface +type jobs struct { + r *ExperimentalClient + ns string +} + +// newJobs returns a jobs +func newJobs(c *ExperimentalClient, namespace string) *jobs { + return &jobs{c, namespace} +} + +// List returns a list of jobs that match the label and field selectors. +func (c *jobs) List(label labels.Selector, field fields.Selector) (result *experimental.JobList, err error) { + result = &experimental.JobList{} + err = c.r.Get().Namespace(c.ns).Resource("jobs").LabelsSelectorParam(label).FieldsSelectorParam(field).Do().Into(result) + return +} + +// Get returns information about a particular job. +func (c *jobs) Get(name string) (result *experimental.Job, err error) { + result = &experimental.Job{} + err = c.r.Get().Namespace(c.ns).Resource("jobs").Name(name).Do().Into(result) + return +} + +// Create creates a new job. +func (c *jobs) Create(job *experimental.Job) (result *experimental.Job, err error) { + result = &experimental.Job{} + err = c.r.Post().Namespace(c.ns).Resource("jobs").Body(job).Do().Into(result) + return +} + +// Update updates an existing job. +func (c *jobs) Update(job *experimental.Job) (result *experimental.Job, err error) { + result = &experimental.Job{} + err = c.r.Put().Namespace(c.ns).Resource("jobs").Name(job.Name).Body(job).Do().Into(result) + return +} + +// Delete deletes a job, returns error if one occurs. +func (c *jobs) Delete(name string, options *api.DeleteOptions) (err error) { + if options == nil { + return c.r.Delete().Namespace(c.ns).Resource("jobs").Name(name).Do().Error() + } + + body, err := api.Scheme.EncodeToVersion(options, c.r.APIVersion()) + if err != nil { + return err + } + return c.r.Delete().Namespace(c.ns).Resource("jobs").Name(name).Body(body).Do().Error() +} + +// Watch returns a watch.Interface that watches the requested jobs. +func (c *jobs) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return c.r.Get(). + Prefix("watch"). + Namespace(c.ns). + Resource("jobs"). + Param("resourceVersion", resourceVersion). + LabelsSelectorParam(label). + FieldsSelectorParam(field). + Watch() +} + +// UpdateStatus takes the name of the job and the new status. Returns the server's representation of the job, and an error, if it occurs. +func (c *jobs) UpdateStatus(job *experimental.Job) (result *experimental.Job, err error) { + result = &experimental.Job{} + err = c.r.Put().Namespace(c.ns).Resource("jobs").Name(job.Name).SubResource("status").Body(job).Do().Into(result) + return +} diff --git a/pkg/client/unversioned/jobs_test.go b/pkg/client/unversioned/jobs_test.go new file mode 100644 index 00000000000..ed79450227d --- /dev/null +++ b/pkg/client/unversioned/jobs_test.go @@ -0,0 +1,222 @@ +/* +Copyright 2015 The Kubernetes Authors 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 unversioned + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/apis/experimental" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" +) + +func getJobResourceName() string { + return "jobs" +} + +func TestListJobs(t *testing.T) { + ns := api.NamespaceAll + c := &testClient{ + Request: testRequest{ + Method: "GET", + Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, ""), + }, + Response: Response{StatusCode: 200, + Body: &experimental.JobList{ + Items: []experimental.Job{ + { + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "name": "baz", + }, + }, + Spec: experimental.JobSpec{ + Template: &api.PodTemplateSpec{}, + }, + }, + }, + }, + }, + } + receivedJobList, err := c.Setup(t).Experimental().Jobs(ns).List(labels.Everything(), fields.Everything()) + c.Validate(t, receivedJobList, err) +} + +func TestGetJob(t *testing.T) { + ns := api.NamespaceDefault + c := &testClient{ + Request: testRequest{ + Method: "GET", + Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, "foo"), + Query: buildQueryValues(nil), + }, + Response: Response{ + StatusCode: 200, + Body: &experimental.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "name": "baz", + }, + }, + Spec: experimental.JobSpec{ + Template: &api.PodTemplateSpec{}, + }, + }, + }, + } + receivedJob, err := c.Setup(t).Experimental().Jobs(ns).Get("foo") + c.Validate(t, receivedJob, err) +} + +func TestGetJobWithNoName(t *testing.T) { + ns := api.NamespaceDefault + c := &testClient{Error: true} + receivedJob, err := c.Setup(t).Experimental().Jobs(ns).Get("") + if (err != nil) && (err.Error() != nameRequiredError) { + t.Errorf("Expected error: %v, but got %v", nameRequiredError, err) + } + + c.Validate(t, receivedJob, err) +} + +func TestUpdateJob(t *testing.T) { + ns := api.NamespaceDefault + requestJob := &experimental.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: ns, + ResourceVersion: "1", + }, + } + c := &testClient{ + Request: testRequest{ + Method: "PUT", + Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, "foo"), + Query: buildQueryValues(nil), + }, + Response: Response{ + StatusCode: 200, + Body: &experimental.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "name": "baz", + }, + }, + Spec: experimental.JobSpec{ + Template: &api.PodTemplateSpec{}, + }, + }, + }, + } + receivedJob, err := c.Setup(t).Experimental().Jobs(ns).Update(requestJob) + c.Validate(t, receivedJob, err) +} + +func TestUpdateJobStatus(t *testing.T) { + ns := api.NamespaceDefault + requestJob := &experimental.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: ns, + ResourceVersion: "1", + }, + } + c := &testClient{ + Request: testRequest{ + Method: "PUT", + Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, "foo") + "/status", + Query: buildQueryValues(nil), + }, + Response: Response{ + StatusCode: 200, + Body: &experimental.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "name": "baz", + }, + }, + Spec: experimental.JobSpec{ + Template: &api.PodTemplateSpec{}, + }, + Status: experimental.JobStatus{ + Active: 1, + }, + }, + }, + } + receivedJob, err := c.Setup(t).Experimental().Jobs(ns).UpdateStatus(requestJob) + c.Validate(t, receivedJob, err) +} + +func TestDeleteJob(t *testing.T) { + ns := api.NamespaceDefault + c := &testClient{ + Request: testRequest{ + Method: "DELETE", + Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, "foo"), + Query: buildQueryValues(nil), + }, + Response: Response{StatusCode: 200}, + } + err := c.Setup(t).Experimental().Jobs(ns).Delete("foo", nil) + c.Validate(t, nil, err) +} + +func TestCreateJob(t *testing.T) { + ns := api.NamespaceDefault + requestJob := &experimental.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: ns, + }, + } + c := &testClient{ + Request: testRequest{ + Method: "POST", + Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, ""), + Body: requestJob, + Query: buildQueryValues(nil), + }, + Response: Response{ + StatusCode: 200, + Body: &experimental.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "name": "baz", + }, + }, + Spec: experimental.JobSpec{ + Template: &api.PodTemplateSpec{}, + }, + }, + }, + } + receivedJob, err := c.Setup(t).Experimental().Jobs(ns).Create(requestJob) + c.Validate(t, receivedJob, err) +} diff --git a/pkg/client/unversioned/testclient/testclient.go b/pkg/client/unversioned/testclient/testclient.go index 7229227f248..6dde3db6703 100644 --- a/pkg/client/unversioned/testclient/testclient.go +++ b/pkg/client/unversioned/testclient/testclient.go @@ -261,3 +261,7 @@ func (c *FakeExperimental) Deployments(namespace string) client.DeploymentInterf func (c *FakeExperimental) Scales(namespace string) client.ScaleInterface { return &FakeScales{Fake: c, Namespace: namespace} } + +func (c *FakeExperimental) Jobs(namespace string) client.JobInterface { + panic("unimplemented") +} diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 310c0acb230..65be81d3d40 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -28,6 +28,7 @@ import ( "github.com/golang/glog" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/resource" + "k8s.io/kubernetes/pkg/apis/experimental" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/fieldpath" "k8s.io/kubernetes/pkg/fields" @@ -85,6 +86,7 @@ func describerMap(c *client.Client) map[string]Describer { func expDescriberMap(c *client.Client) map[string]Describer { return map[string]Describer{ "HorizontalPodAutoscaler": &HorizontalPodAutoscalerDescriber{c}, + "Job": &JobDescriber{c}, } } @@ -854,6 +856,46 @@ func describeReplicationController(controller *api.ReplicationController, events }) } +// JobDescriber generates information about a job and the pods it has created. +type JobDescriber struct { + client *client.Client +} + +func (d *JobDescriber) Describe(namespace, name string) (string, error) { + job, err := d.client.Experimental().Jobs(namespace).Get(name) + if err != nil { + return "", err + } + + events, _ := d.client.Events(namespace).Search(job) + + return describeJob(job, events) +} + +func describeJob(job *experimental.Job, events *api.EventList) (string, error) { + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "Name:\t%s\n", job.Name) + fmt.Fprintf(out, "Namespace:\t%s\n", job.Namespace) + if job.Spec.Template != nil { + fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&job.Spec.Template.Spec)) + } else { + fmt.Fprintf(out, "Image(s):\t%s\n", "") + } + fmt.Fprintf(out, "Selector:\t%s\n", labels.FormatLabels(job.Spec.Selector)) + fmt.Fprintf(out, "Parallelism:\t%d\n", job.Spec.Parallelism) + fmt.Fprintf(out, "Completions:\t%d\n", job.Spec.Completions) + fmt.Fprintf(out, "Labels:\t%s\n", labels.FormatLabels(job.Labels)) + fmt.Fprintf(out, "Pods Statuses:\t%d Running / %d Succeeded / %d Failed\n", job.Status.Active, job.Status.Successful, job.Status.Unsuccessful) + if job.Spec.Template != nil { + describeVolumes(job.Spec.Template.Spec.Volumes, out) + } + if events != nil { + DescribeEvents(events, out) + } + return nil + }) +} + // SecretDescriber generates information about a secret type SecretDescriber struct { client.Interface diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index e8c18d13385..81f095cd01f 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -380,6 +380,7 @@ func (h *HumanReadablePrinter) HandledResources() []string { var podColumns = []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE"} var podTemplateColumns = []string{"TEMPLATE", "CONTAINER(S)", "IMAGE(S)", "PODLABELS"} var replicationControllerColumns = []string{"CONTROLLER", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "REPLICAS", "AGE"} +var jobColumns = []string{"JOB", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "SUCCESSFUL"} var serviceColumns = []string{"NAME", "CLUSTER_IP", "EXTERNAL_IP", "PORT(S)", "SELECTOR", "AGE"} var endpointColumns = []string{"NAME", "ENDPOINTS", "AGE"} var nodeColumns = []string{"NAME", "LABELS", "STATUS", "AGE"} @@ -405,6 +406,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(podTemplateColumns, printPodTemplateList) h.Handler(replicationControllerColumns, printReplicationController) h.Handler(replicationControllerColumns, printReplicationControllerList) + h.Handler(jobColumns, printJob) + h.Handler(jobColumns, printJobList) h.Handler(serviceColumns, printService) h.Handler(serviceColumns, printServiceList) h.Handler(endpointColumns, printEndpoints) @@ -655,7 +658,6 @@ func printPodTemplateList(podList *api.PodTemplateList, w io.Writer, withNamespa func printReplicationController(controller *api.ReplicationController, w io.Writer, withNamespace bool, wide bool, showAll bool, columnLabels []string) error { name := controller.Name namespace := controller.Namespace - containers := controller.Spec.Template.Spec.Containers var firstContainer api.Container if len(containers) > 0 { @@ -707,6 +709,33 @@ func printReplicationControllerList(list *api.ReplicationControllerList, w io.Wr return nil } +func printJob(job *experimental.Job, w io.Writer, withNamespace bool, wide bool, showAll bool, columnLabels []string) error { + containers := job.Spec.Template.Spec.Containers + var firstContainer api.Container + if len(containers) > 0 { + firstContainer = containers[0] + } + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", + job.Name, + firstContainer.Name, + firstContainer.Image, + labels.FormatLabels(job.Spec.Selector), + job.Status.Successful) + if err != nil { + return err + } + return nil +} + +func printJobList(list *experimental.JobList, w io.Writer, withNamespace bool, wide bool, showAll bool, columnLabels []string) error { + for _, job := range list.Items { + if err := printJob(&job, w, withNamespace, wide, showAll, columnLabels); err != nil { + return err + } + } + return nil +} + func getServiceExternalIP(svc *api.Service) string { switch svc.Spec.Type { case api.ServiceTypeClusterIP: diff --git a/pkg/registry/job/doc.go b/pkg/registry/job/doc.go index a76a224cbff..d6351371c5a 100644 --- a/pkg/registry/job/doc.go +++ b/pkg/registry/job/doc.go @@ -14,6 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package controller provides Registry interface and it's RESTStorage +// Package job provides Registry interface and it's RESTStorage // implementation for storing Job api objects. package job diff --git a/pkg/registry/job/etcd/etcd.go b/pkg/registry/job/etcd/etcd.go index b6ff7bd7750..f6e955c3cb8 100644 --- a/pkg/registry/job/etcd/etcd.go +++ b/pkg/registry/job/etcd/etcd.go @@ -28,7 +28,7 @@ import ( "k8s.io/kubernetes/pkg/storage" ) -// rest implements a RESTStorage for jobs against etcd +// REST implements a RESTStorage for jobs against etcd type REST struct { *etcdgeneric.Etcd }