Repository: knative/pkg
Pull request #2299 (2021-09-27 23:31:00)

duck cronjob
<!-- Thanks for sending a pull request! -->

:gift: Create a ducktype for CronJob so that we can validate it using our webhooks framework.

Needed for:
https://github.com/sigstore/cosign/issues/802

<!-- 
Describe your changes here- ideally you can get that description straight from
your descriptive commit message(s)! 

- :gift: Add new feature
- :bug: Fix bug
- :broom: Update or clean up current behavior
- :wastebasket: Remove feature or internal logic
-->

-
-
-

<!--
In addition, categorize the changes you're making using the "/kind" Prow command, example:

/kind <kind>

Supported kinds are: api-change, bug, cleanup, deprecation, removal, documentation, enhancement, performance

-->
/kind <kind>

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

<!-- Please include the 'why' behind your changes if no issue exists -->

**Release Note**

<!--
:page_facing_up: If this change has user-visible impact, write a release note in the block
below. Include the string "action required" if additional action is required of
users switching to the new release, for example in case of a breaking change.

Write as if you are speaking to users, not other Knative contributors. If this
change has no user-visible impact, no release-note is needed.
-->
```release-note
:gift: Create a ducktype for CronJob so that we can validate it using our webhooks framework.
```

**Docs**

<!--
:book: If this change has user-visible impact, link to an issue or PR in
https://github.com/knative/docs.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [knative/docs]: <issue or pr link>
- [Feature Track]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```


knative-prow-robot: @vaikas: The label(s) `kind/<kind>` cannot be applied, because the repository doesn't have them.

<details>

In response to [this](https://github.com/knative/pkg/pull/2299):

>
>
>:gift: Create a ducktype for CronJob so that we can validate it using our webhooks framework.
>
>Needed for:
>https://github.com/sigstore/cosign/issues/802
>
>
>
>-
>-
>-
>
>
>/kind <kind>
>
>
>Fixes #
>
>
>
>**Release Note**
>
>
>```release-note
>:gift: Create a ducktype for CronJob so that we can validate it using our webhooks framework.
>```
>
>**Docs**
>
>
>```docs
>
>```
>


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
knative-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/2299#" title="Author self-approved">vaikas</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative%2Fpkg).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[apis/duck/OWNERS](https://github.com/knative/pkg/blob/main/apis/duck/OWNERS)~~ [vaikas]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
mattmoor: /lgtm
/approve
knative-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/2299#issuecomment-928462508" title="Approved">mattmoor</a>*, *<a href="https://github.com/knative/pkg/pull/2299#" title="Author self-approved">vaikas</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative%2Fpkg).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[apis/duck/OWNERS](https://github.com/knative/pkg/blob/main/apis/duck/OWNERS)~~ [mattmoor,vaikas]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
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_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/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


Test patch:
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_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)
+			}
+		})
+	}
+}