Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduledjob e2e #26027

Merged
merged 4 commits into from
Aug 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cmd/kube-controller-manager/app/controllermanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"k8s.io/kubernetes/cmd/kube-controller-manager/app/options"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/batch"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/leaderelection"
"k8s.io/kubernetes/pkg/client/record"
Expand Down Expand Up @@ -128,6 +129,7 @@ func Run(s *options.CMServer) error {
}

kubeconfig.ContentConfig.ContentType = s.ContentType
kubeconfig.ContentConfig.GroupVersion = &unversioned.GroupVersion{Group: batch.GroupName, Version: "v2alpha1"}
// Override kubeconfig qps/burst settings from flags
kubeconfig.QPS = s.KubeAPIQPS
kubeconfig.Burst = int(s.KubeAPIBurst)
Expand Down Expand Up @@ -390,7 +392,7 @@ func StartControllers(s *options.CMServer, kubeClient *client.Client, kubeconfig
glog.Infof("Starting %s apis", groupVersion)
if containsResource(resources, "scheduledjobs") {
glog.Infof("Starting scheduledjob controller")
go scheduledjob.NewScheduledJobController(clientset.NewForConfigOrDie(restclient.AddUserAgent(kubeconfig, "scheduledjob-controller"))).
go scheduledjob.NewScheduledJobController(kubeClient).
Run(wait.NeverStop)
time.Sleep(wait.Jitter(s.ControllerStartInterval.Duration, ControllerStartJitter))
time.Sleep(wait.Jitter(s.ControllerStartInterval.Duration, ControllerStartJitter))
Expand Down
2 changes: 2 additions & 0 deletions hack/e2e.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ func TearDown() bool {

// Up brings an e2e cluster up, recreating it if one is already running.
func Up() bool {
// force having batch/v2alpha1 always on for e2e tests
os.Setenv("KUBE_RUNTIME_CONFIG", "batch/v2alpha1=true")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that this is DRY'er than setting a default in all of the clusters/*/config-foo.sh scripts but I don't know if it's a pattern we want to extend as more features get added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that's why I asked. Again with batch/v2alpha1 I'm blazing the trail, this time for group versions for e2e tests.

return finishRunning("up", exec.Command("./hack/e2e-internal/e2e-up.sh"))
}

Expand Down
15 changes: 7 additions & 8 deletions pkg/controller/scheduledjob/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/batch"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
unversionedcore "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/unversioned"
"k8s.io/kubernetes/pkg/client/record"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/controller/job"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/metrics"
Expand All @@ -50,20 +49,20 @@ import (
// Utilities for dealing with Jobs and ScheduledJobs and time.

type ScheduledJobController struct {
kubeClient clientset.Interface
kubeClient *client.Client
jobControl jobControlInterface
sjControl sjControlInterface
recorder record.EventRecorder
}

func NewScheduledJobController(kubeClient clientset.Interface) *ScheduledJobController {
func NewScheduledJobController(kubeClient *client.Client) *ScheduledJobController {
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(glog.Infof)
// TODO: remove the wrapper when every clients have moved to use the clientset.
eventBroadcaster.StartRecordingToSink(&unversionedcore.EventSinkImpl{Interface: kubeClient.Core().Events("")})
eventBroadcaster.StartRecordingToSink(kubeClient.Events(""))

if kubeClient != nil && kubeClient.Batch().GetRESTClient().GetRateLimiter() != nil {
metrics.RegisterMetricAndTrackRateLimiterUsage("scheduledjob_controller", kubeClient.Batch().GetRESTClient().GetRateLimiter())
if kubeClient != nil && kubeClient.GetRateLimiter() != nil {
metrics.RegisterMetricAndTrackRateLimiterUsage("scheduledjob_controller", kubeClient.GetRateLimiter())
}

jm := &ScheduledJobController{
Expand All @@ -76,7 +75,7 @@ func NewScheduledJobController(kubeClient clientset.Interface) *ScheduledJobCont
return jm
}

func NewScheduledJobControllerFromClient(kubeClient clientset.Interface) *ScheduledJobController {
func NewScheduledJobControllerFromClient(kubeClient *client.Client) *ScheduledJobController {
jm := NewScheduledJobController(kubeClient)
return jm
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/controller/scheduledjob/injection.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import (
"sync"

"k8s.io/kubernetes/pkg/apis/batch"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/record"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/labels"
)

Expand All @@ -33,7 +33,7 @@ type sjControlInterface interface {

// realSJControl is the default implementation of sjControlInterface.
type realSJControl struct {
KubeClient clientset.Interface
KubeClient *client.Client
}

var _ sjControlInterface = &realSJControl{}
Expand Down Expand Up @@ -69,7 +69,7 @@ type jobControlInterface interface {

// realJobControl is the default implementation of jobControlInterface.
type realJobControl struct {
KubeClient clientset.Interface
KubeClient *client.Client
Recorder record.EventRecorder
}

Expand Down
9 changes: 7 additions & 2 deletions test/e2e/framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_4"
"k8s.io/kubernetes/pkg/api"
apierrs "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/client/clientset_generated/release_1_2"
"k8s.io/kubernetes/pkg/client/clientset_generated/release_1_3"
"k8s.io/kubernetes/pkg/client/restclient"
Expand Down Expand Up @@ -103,8 +104,9 @@ type TestDataSummary interface {
}

type FrameworkOptions struct {
ClientQPS float32
ClientBurst int
ClientQPS float32
ClientBurst int
GroupVersion *unversioned.GroupVersion
}

// NewFramework makes a new framework and sets up a BeforeEach/AfterEach for
Expand Down Expand Up @@ -180,6 +182,9 @@ func (f *Framework) BeforeEach() {
Expect(err).NotTo(HaveOccurred())
config.QPS = f.options.ClientQPS
config.Burst = f.options.ClientBurst
if f.options.GroupVersion != nil {
config.GroupVersion = f.options.GroupVersion
}
if TestContext.KubeAPIContentType != "" {
config.ContentType = TestContext.KubeAPIContentType
}
Expand Down
245 changes: 245 additions & 0 deletions test/e2e/scheduledjob.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/*
Copyright 2016 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 e2e

import (
"fmt"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/batch"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/test/e2e/framework"
)

const (
// How long to wait for a scheduledjob
scheduledJobTimeout = 5 * time.Minute
)

var _ = framework.KubeDescribe("ScheduledJob", func() {
options := framework.FrameworkOptions{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or can we do something like:

f := framework.NewDefaultFramework("scheduledjob")
f.GroupVersion = &unversioned.GroupVersion{Group: batch.GroupName, Version: "v2alpha1"}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd propose leave this as is for now, the end goal is to entirely get rid of the unversioned client, then the problem should disappear and SJ is first on my list, but that will be not sooner than 1.5.

ClientQPS: 20,
ClientBurst: 50,
GroupVersion: &unversioned.GroupVersion{Group: batch.GroupName, Version: "v2alpha1"},
}
f := framework.NewFramework("scheduledjob", options, nil)

// multiple jobs running at once
It("should schedule multiple jobs concurrently", func() {
By("Creating a scheduledjob")
scheduledJob := newTestScheduledJob("concurrent", "*/1 * * * ?", batch.AllowConcurrent)
scheduledJob, err := createScheduledJob(f.Client, f.Namespace.Name, scheduledJob)
Expect(err).NotTo(HaveOccurred())

By("Ensuring more than one job is running at a time")
err = waitForActiveJobs(f.Client, f.Namespace.Name, scheduledJob.Name, 2)
Expect(err).NotTo(HaveOccurred())

By("Ensuring exactly two jobs exists by listing jobs explicitly")
jobs, err := f.Client.Batch().Jobs(f.Namespace.Name).List(api.ListOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(len(jobs.Items) >= 2).To(BeTrue())

By("Removing scheduledjob")
err = deleteScheduledJob(f.Client, f.Namespace.Name, scheduledJob.Name)
Expect(err).NotTo(HaveOccurred())
})

// suspended should not schedule jobs
It("should not schedule jobs when suspended", func() {
By("Creating a suspended scheduledjob")
scheduledJob := newTestScheduledJob("suspended", "*/1 * * * ?", batch.AllowConcurrent)
scheduledJob.Spec.Suspend = newBool(true)
scheduledJob, err := createScheduledJob(f.Client, f.Namespace.Name, scheduledJob)
Expect(err).NotTo(HaveOccurred())

By("Ensuring no jobs are scheduled")
err = waitForNoJobs(f.Client, f.Namespace.Name, scheduledJob.Name)
Expect(err).To(HaveOccurred())

By("Ensuring no job exists by listing jobs explicitly")
jobs, err := f.Client.Batch().Jobs(f.Namespace.Name).List(api.ListOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(jobs.Items).To(HaveLen(0))

By("Removing scheduledjob")
err = deleteScheduledJob(f.Client, f.Namespace.Name, scheduledJob.Name)
Expect(err).NotTo(HaveOccurred())
})

// only single active job is allowed for ForbidConcurrent
It("should not schedule new jobs when ForbidConcurrent", func() {
By("Creating a ForbidConcurrent scheduledjob")
scheduledJob := newTestScheduledJob("forbid", "*/1 * * * ?", batch.ForbidConcurrent)
scheduledJob, err := createScheduledJob(f.Client, f.Namespace.Name, scheduledJob)
Expect(err).NotTo(HaveOccurred())

By("Ensuring a job is scheduled")
err = waitForActiveJobs(f.Client, f.Namespace.Name, scheduledJob.Name, 1)
Expect(err).NotTo(HaveOccurred())

By("Ensuring exactly one is scheduled")
scheduledJob, err = getScheduledJob(f.Client, f.Namespace.Name, scheduledJob.Name)
Expect(err).NotTo(HaveOccurred())
Expect(scheduledJob.Status.Active).Should(HaveLen(1))

By("Ensuring exaclty one job exists by listing jobs explicitly")
jobs, err := f.Client.Batch().Jobs(f.Namespace.Name).List(api.ListOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(jobs.Items).To(HaveLen(1))

By("Ensuring no more jobs are scheduled")
err = waitForActiveJobs(f.Client, f.Namespace.Name, scheduledJob.Name, 2)
Expect(err).To(HaveOccurred())

By("Removing scheduledjob")
err = deleteScheduledJob(f.Client, f.Namespace.Name, scheduledJob.Name)
Expect(err).NotTo(HaveOccurred())
})

// only single active job is allowed for ReplaceConcurrent
It("should replace jobs when ReplaceConcurrent", func() {
By("Creating a ReplaceConcurrent scheduledjob")
scheduledJob := newTestScheduledJob("replace", "*/1 * * * ?", batch.ReplaceConcurrent)
scheduledJob, err := createScheduledJob(f.Client, f.Namespace.Name, scheduledJob)
Expect(err).NotTo(HaveOccurred())

By("Ensuring a job is scheduled")
err = waitForActiveJobs(f.Client, f.Namespace.Name, scheduledJob.Name, 1)
Expect(err).NotTo(HaveOccurred())

By("Ensuring exactly one is scheduled")
scheduledJob, err = getScheduledJob(f.Client, f.Namespace.Name, scheduledJob.Name)
Expect(err).NotTo(HaveOccurred())
Expect(scheduledJob.Status.Active).Should(HaveLen(1))

By("Ensuring exaclty one job exists by listing jobs explicitly")
jobs, err := f.Client.Batch().Jobs(f.Namespace.Name).List(api.ListOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(jobs.Items).To(HaveLen(1))

By("Ensuring the job is replaced with a new one")
err = waitForJobReplaced(f.Client, f.Namespace.Name, jobs.Items[0].Name)
Expect(err).NotTo(HaveOccurred())

By("Removing scheduledjob")
err = deleteScheduledJob(f.Client, f.Namespace.Name, scheduledJob.Name)
Expect(err).NotTo(HaveOccurred())
})
})

// newTestScheduledJob returns a scheduledjob which does one of several testing behaviors.
func newTestScheduledJob(name, schedule string, concurrencyPolicy batch.ConcurrencyPolicy) *batch.ScheduledJob {
parallelism := int32(1)
completions := int32(1)
return &batch.ScheduledJob{
ObjectMeta: api.ObjectMeta{
Name: name,
},
Spec: batch.ScheduledJobSpec{
Schedule: schedule,
ConcurrencyPolicy: concurrencyPolicy,
JobTemplate: batch.JobTemplateSpec{
Spec: batch.JobSpec{
Parallelism: &parallelism,
Completions: &completions,
Template: api.PodTemplateSpec{
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyOnFailure,
Volumes: []api.Volume{
{
Name: "data",
VolumeSource: api.VolumeSource{
EmptyDir: &api.EmptyDirVolumeSource{},
},
},
},
Containers: []api.Container{
{
Name: "c",
Image: "gcr.io/google_containers/busybox:1.24",
Command: []string{"sleep", "300"},
VolumeMounts: []api.VolumeMount{
{
MountPath: "/data",
Name: "data",
},
},
},
},
},
},
},
},
},
}
}

func createScheduledJob(c *client.Client, ns string, scheduledJob *batch.ScheduledJob) (*batch.ScheduledJob, error) {
return c.Batch().ScheduledJobs(ns).Create(scheduledJob)
}

func getScheduledJob(c *client.Client, ns, name string) (*batch.ScheduledJob, error) {
return c.Batch().ScheduledJobs(ns).Get(name)
}

func deleteScheduledJob(c *client.Client, ns, name string) error {
return c.Batch().ScheduledJobs(ns).Delete(name, nil)
}

// Wait for at least given amount of active jobs.
func waitForActiveJobs(c *client.Client, ns, scheduledJobName string, active int) error {
return wait.Poll(framework.Poll, scheduledJobTimeout, func() (bool, error) {
curr, err := c.Batch().ScheduledJobs(ns).Get(scheduledJobName)
if err != nil {
return false, err
}
return len(curr.Status.Active) >= active, nil
})
}

// Wait for no jobs to appear.
func waitForNoJobs(c *client.Client, ns, jobName string) error {
return wait.Poll(framework.Poll, scheduledJobTimeout, func() (bool, error) {
curr, err := c.Batch().ScheduledJobs(ns).Get(jobName)
if err != nil {
return false, err
}

return len(curr.Status.Active) != 0, nil
})
}

// Wait for a job to be replaced with a new one.
func waitForJobReplaced(c *client.Client, ns, previousJobName string) error {
return wait.Poll(framework.Poll, scheduledJobTimeout, func() (bool, error) {
jobs, err := c.Batch().Jobs(ns).List(api.ListOptions{})
if err != nil {
return false, err
}
if len(jobs.Items) != 1 {
return false, fmt.Errorf("More than one job is running")
}
return jobs.Items[0].Name != previousJobName, nil
})
}