duck cronjob (#2299)

This commit is contained in:
Ville Aikas 2021-09-27 16:50:13 -07:00 committed by GitHub
parent f07c8e42ae
commit 221312a6a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 443 additions and 0 deletions

View File

@ -0,0 +1,47 @@
/*
Copyright 2021 The Knative 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 v1
import (
"context"
)
// CronJobDefaulter is a callback to validate a CronJob.
type CronJobDefaulter func(context.Context, *CronJob)
// SetDefaults implements apis.Defaultable
func (c *CronJob) SetDefaults(ctx context.Context) {
if cd := GetCronJobDefaulter(ctx); cd != nil {
cd(ctx, c)
}
}
// cdKey is used for associating a CronJobDefaulter with a context.Context
type cdKey struct{}
func WithCronJobDefaulter(ctx context.Context, cd CronJobDefaulter) context.Context {
return context.WithValue(ctx, cdKey{}, cd)
}
// GetCronJobDefaulter extracts the CronJobDefaulter from the context.
func GetCronJobDefaulter(ctx context.Context) CronJobDefaulter {
untyped := ctx.Value(cdKey{})
if untyped == nil {
return nil
}
return untyped.(CronJobDefaulter)
}

View File

@ -0,0 +1,108 @@
/*
Copyright 2021 The Knative 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 v1
import (
"context"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
)
func TestCronJobDefaulting(t *testing.T) {
c := CronJob{
Spec: batchv1.CronJobSpec{
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "blah",
Image: "busybox",
}},
},
},
},
},
},
}
tests := []struct {
name string
with func(context.Context) context.Context
want *CronJob
}{{
name: "no check",
with: func(ctx context.Context) context.Context {
return ctx
},
want: c.DeepCopy(),
}, {
name: "no change",
with: func(ctx context.Context) context.Context {
return WithCronJobDefaulter(ctx, func(ctx context.Context, c *CronJob) {
})
},
want: c.DeepCopy(),
}, {
name: "no busybox",
with: func(ctx context.Context) context.Context {
return WithCronJobDefaulter(ctx, func(ctx context.Context, c *CronJob) {
for i, con := range c.Spec.JobTemplate.Spec.Template.Spec.InitContainers {
if !strings.Contains(con.Image, "@") {
c.Spec.JobTemplate.Spec.Template.Spec.InitContainers[i].Image = con.Image + "@sha256:deadbeef"
}
}
for i, con := range c.Spec.JobTemplate.Spec.Template.Spec.Containers {
if !strings.Contains(con.Image, "@") {
c.Spec.JobTemplate.Spec.Template.Spec.Containers[i].Image = con.Image + "@sha256:deadbeef"
}
}
})
},
want: &CronJob{
Spec: batchv1.CronJobSpec{
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "blah",
Image: "busybox@sha256:deadbeef",
}},
},
},
},
},
},
},
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := test.with(context.Background())
got := c.DeepCopy()
got.SetDefaults(ctx)
if !cmp.Equal(test.want, got) {
t.Errorf("SetDefaults (-want, +got) = %s", cmp.Diff(test.want, got))
}
})
}
}

View File

@ -0,0 +1,86 @@
/*
Copyright 2021 The Knative 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 v1
import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"knative.dev/pkg/apis"
"knative.dev/pkg/apis/duck/ducktypes"
)
// +genduck
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// CronJob is a wrapper around CronJob resource, which supports our interfaces
// for webhooks
type CronJob struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec batchv1.CronJobSpec `json:"spec,omitempty"`
}
// Verify CronJob resources meet duck contracts.
var (
_ apis.Validatable = (*CronJob)(nil)
_ apis.Defaultable = (*CronJob)(nil)
_ apis.Listable = (*CronJob)(nil)
_ ducktypes.Populatable = (*CronJob)(nil)
)
// GetFullType implements duck.Implementable
func (c *CronJob) GetFullType() ducktypes.Populatable {
return &CronJob{}
}
// Populate implements duck.Populatable
func (c *CronJob) Populate() {
c.Spec = batchv1.CronJobSpec{
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "container-name",
Image: "container-image:latest",
}},
},
},
},
},
}
}
// GetListType implements apis.Listable
func (c *CronJob) GetListType() runtime.Object {
return &CronJob{}
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// CronJobList is a list of CronJob resources
type CronJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []CronJob `json:"items"`
}

View File

@ -0,0 +1,50 @@
/*
Copyright 2021 The Knative 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 v1
import (
"context"
"knative.dev/pkg/apis"
)
// CronJobValidator is a callback to validate a CronJob.
type CronJobValidator func(context.Context, *CronJob) *apis.FieldError
// Validate implements apis.Validatable
func (c *CronJob) Validate(ctx context.Context) *apis.FieldError {
if cv := GetCronJobValidator(ctx); cv != nil {
return cv(ctx, c)
}
return nil
}
// cvKey is used for associating a CronJobValidator with a context.Context
type cvKey struct{}
func WithCronJobValidator(ctx context.Context, cv CronJobValidator) context.Context {
return context.WithValue(ctx, cvKey{}, cv)
}
// GetCronJobValidator extracts the CronJobValidator from the context.
func GetCronJobValidator(ctx context.Context) CronJobValidator {
untyped := ctx.Value(cvKey{})
if untyped == nil {
return nil
}
return untyped.(CronJobValidator)
}

View File

@ -0,0 +1,92 @@
/*
Copyright 2021 The Knative 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 v1
import (
"context"
"testing"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"knative.dev/pkg/apis"
)
func TestCronJobValidation(t *testing.T) {
tests := []struct {
name string
with func(context.Context) context.Context
want *apis.FieldError
}{{
name: "no check",
with: func(ctx context.Context) context.Context {
return ctx
},
want: nil,
}, {
name: "no error",
with: func(ctx context.Context) context.Context {
return WithCronJobValidator(ctx, func(ctx context.Context, c *CronJob) *apis.FieldError {
return nil
})
},
want: nil,
}, {
name: "no busybox",
with: func(ctx context.Context) context.Context {
return WithCronJobValidator(ctx, func(ctx context.Context, c *CronJob) *apis.FieldError {
for i, con := range c.Spec.JobTemplate.Spec.Template.Spec.InitContainers {
if con.Image == "busybox" {
return apis.ErrInvalidValue(con.Image, "image").ViaFieldIndex("spec.template.spec.initContainers", i)
}
}
for i, con := range c.Spec.JobTemplate.Spec.Template.Spec.Containers {
if con.Image == "busybox" {
return apis.ErrInvalidValue(con.Image, "image").ViaFieldIndex("spec.template.spec.containers", i)
}
}
return nil
})
},
want: apis.ErrInvalidValue("busybox", "spec.template.spec.containers[0].image"),
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := CronJob{
Spec: batchv1.CronJobSpec{
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "blah",
Image: "busybox",
}},
},
},
},
},
},
}
ctx := test.with(context.Background())
got := c.Validate(ctx)
if test.want.Error() != got.Error() {
t.Errorf("Validate() = %v, wanted %v", got, test.want)
}
})
}
}

View File

@ -265,6 +265,66 @@ func (in Conditions) DeepCopy() Conditions {
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CronJob) DeepCopyInto(out *CronJob) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJob.
func (in *CronJob) DeepCopy() *CronJob {
if in == nil {
return nil
}
out := new(CronJob)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CronJob) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CronJobList) DeepCopyInto(out *CronJobList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]CronJob, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobList.
func (in *CronJobList) DeepCopy() *CronJobList {
if in == nil {
return nil
}
out := new(CronJobList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CronJobList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Destination) DeepCopyInto(out *Destination) {
*out = *in