Merge pull request #13254 from soltysh/job_controller_kubectl

Job resource - kubectl part
This commit is contained in:
Jeff Lowdermilk 2015-09-15 17:45:14 -07:00
commit b2c74a7297
9 changed files with 420 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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", "<no template>")
}
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

View File

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

View File

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

View File

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