diff --git a/apis/duck/v1/cronjob_defaults.go b/apis/duck/v1/cronjob_defaults.go new file mode 100644 index 000000000..94ef552c0 --- /dev/null +++ b/apis/duck/v1/cronjob_defaults.go @@ -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) +} diff --git a/apis/duck/v1/cronjob_defaults_test.go b/apis/duck/v1/cronjob_defaults_test.go new file mode 100644 index 000000000..74120f7b2 --- /dev/null +++ b/apis/duck/v1/cronjob_defaults_test.go @@ -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)) + } + }) + } +} diff --git a/apis/duck/v1/cronjob_types.go b/apis/duck/v1/cronjob_types.go new file mode 100644 index 000000000..f0f262591 --- /dev/null +++ b/apis/duck/v1/cronjob_types.go @@ -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"` +} diff --git a/apis/duck/v1/cronjob_validation.go b/apis/duck/v1/cronjob_validation.go new file mode 100644 index 000000000..51834018b --- /dev/null +++ b/apis/duck/v1/cronjob_validation.go @@ -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) +} diff --git a/apis/duck/v1/cronjob_validation_test.go b/apis/duck/v1/cronjob_validation_test.go new file mode 100644 index 000000000..e4c23bb7e --- /dev/null +++ b/apis/duck/v1/cronjob_validation_test.go @@ -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) + } + }) + } +} diff --git a/apis/duck/v1/zz_generated.deepcopy.go b/apis/duck/v1/zz_generated.deepcopy.go index 22088b214..1894fc2ce 100644 --- a/apis/duck/v1/zz_generated.deepcopy.go +++ b/apis/duck/v1/zz_generated.deepcopy.go @@ -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