Introduce `duckv1.Pod`. (#2280)

This is mostly for symmetry since many folks that validate PodSpecable types often also want to validate `Pod`, so while `Pod` isn't as often a duck-type, the main value of this is exposing similar mechanisms to #2279 for `corev1.Pod` without folks needing to define their own `corev1.Pod` clone.
This commit is contained in:
Matt Moore 2021-09-07 19:59:33 -07:00 committed by GitHub
parent 26db1ba732
commit 71508fc69a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 290 additions and 4 deletions

View File

@ -21,6 +21,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"knative.dev/pkg/apis/duck"
"knative.dev/pkg/apis/duck/ducktypes"
@ -33,6 +34,7 @@ func TestTypesImplements(t *testing.T) {
}{
{instance: &AddressableType{}, iface: &Addressable{}},
{instance: &KResource{}, iface: &Conditions{}},
{instance: &corev1.Pod{}, iface: &Pod{}},
}
for _, tc := range testCases {
if err := duck.VerifyType(tc.instance, tc.iface); err != nil {

View File

@ -30,7 +30,7 @@ func (wp *WithPod) SetDefaults(ctx context.Context) {
}
}
// psvKey is used for associating a PodSpecDefaulter with a context.Context
// psdKey is used for associating a PodSpecDefaulter with a context.Context
type psdKey struct{}
func WithPodSpecDefaulter(ctx context.Context, psd PodSpecDefaulter) context.Context {
@ -45,3 +45,29 @@ func GetPodSpecDefaulter(ctx context.Context) PodSpecDefaulter {
}
return untyped.(PodSpecDefaulter)
}
// PodDefaulter is a callback to validate a Pod.
type PodDefaulter func(context.Context, *Pod)
// SetDefaults implements apis.Defaultable
func (p *Pod) SetDefaults(ctx context.Context) {
if pd := GetPodDefaulter(ctx); pd != nil {
pd(ctx, p)
}
}
// pdKey is used for associating a PodDefaulter with a context.Context
type pdKey struct{}
func WithPodDefaulter(ctx context.Context, pd PodDefaulter) context.Context {
return context.WithValue(ctx, pdKey{}, pd)
}
// GetPodDefaulter extracts the PodDefaulter from the context.
func GetPodDefaulter(ctx context.Context) PodDefaulter {
untyped := ctx.Value(pdKey{})
if untyped == nil {
return nil
}
return untyped.(PodDefaulter)
}

View File

@ -97,3 +97,68 @@ func TestPodSpecDefaulting(t *testing.T) {
})
}
}
func TestPodDefaulting(t *testing.T) {
p := Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "blah",
Image: "busybox",
}},
},
}
tests := []struct {
name string
with func(context.Context) context.Context
want *Pod
}{{
name: "no check",
with: func(ctx context.Context) context.Context {
return ctx
},
want: p.DeepCopy(),
}, {
name: "no change",
with: func(ctx context.Context) context.Context {
return WithPodDefaulter(ctx, func(ctx context.Context, wp *Pod) {
})
},
want: p.DeepCopy(),
}, {
name: "no busybox",
with: func(ctx context.Context) context.Context {
return WithPodDefaulter(ctx, func(ctx context.Context, wp *Pod) {
for i, c := range wp.Spec.InitContainers {
if !strings.Contains(c.Image, "@") {
wp.Spec.InitContainers[i].Image = c.Image + "@sha256:deadbeef"
}
}
for i, c := range wp.Spec.Containers {
if !strings.Contains(c.Image, "@") {
wp.Spec.Containers[i].Image = c.Image + "@sha256:deadbeef"
}
}
})
},
want: &Pod{
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 := p.DeepCopy()
got.SetDefaults(ctx)
if !cmp.Equal(test.want, got) {
t.Errorf("SetDefaults (-want, +got) = %s", cmp.Diff(test.want, got))
}
})
}
}

View File

@ -45,9 +45,6 @@ type WithPod struct {
Spec WithPodSpec `json:"spec,omitempty"`
}
var _ apis.Validatable = (*WithPod)(nil)
var _ apis.Defaultable = (*WithPod)(nil)
// WithPodSpec is the shell around the PodSpecable within WithPod.
type WithPodSpec struct {
Template PodSpecable `json:"template,omitempty"`
@ -55,6 +52,8 @@ type WithPodSpec struct {
// Verify WithPod resources meet duck contracts.
var (
_ apis.Validatable = (*WithPod)(nil)
_ apis.Defaultable = (*WithPod)(nil)
_ apis.Listable = (*WithPod)(nil)
_ ducktypes.Populatable = (*WithPod)(nil)
)
@ -95,3 +94,52 @@ type WithPodList struct {
Items []WithPod `json:"items"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Pod is a wrapper around Pod-like resource, which supports our interfaces
// for webhooks
type Pod struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec corev1.PodSpec `json:"spec,omitempty"`
}
// Verify Pod resources meet duck contracts.
var (
_ apis.Validatable = (*Pod)(nil)
_ apis.Defaultable = (*Pod)(nil)
_ apis.Listable = (*Pod)(nil)
_ ducktypes.Populatable = (*Pod)(nil)
)
// GetFullType implements duck.Implementable
func (p *Pod) GetFullType() ducktypes.Populatable {
return &Pod{}
}
// Populate implements duck.Populatable
func (p *Pod) Populate() {
p.Spec = corev1.PodSpec{
Containers: []corev1.Container{{
Name: "container-name",
Image: "container-image:latest",
}},
}
}
// GetListType implements apis.Listable
func (p *Pod) GetListType() runtime.Object {
return &PodList{}
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// PodList is a list of WithPod resources
type PodList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []WithPod `json:"items"`
}

View File

@ -48,3 +48,30 @@ func GetPodSpecValidator(ctx context.Context) PodSpecValidator {
}
return untyped.(PodSpecValidator)
}
// PodValidator is a callback to validate Pods.
type PodValidator func(context.Context, *Pod) *apis.FieldError
// Validate implements apis.Validatable
func (p *Pod) Validate(ctx context.Context) *apis.FieldError {
if pv := GetPodValidator(ctx); pv != nil {
return pv(ctx, p)
}
return nil
}
// pvKey is used for associating a PodValidator with a context.Context
type pvKey struct{}
func WithPodValidator(ctx context.Context, pv PodValidator) context.Context {
return context.WithValue(ctx, pvKey{}, pv)
}
// GetPodValidator extracts the PodValidator from the context.
func GetPodValidator(ctx context.Context) PodValidator {
untyped := ctx.Value(pvKey{})
if untyped == nil {
return nil
}
return untyped.(PodValidator)
}

View File

@ -85,3 +85,61 @@ func TestPodSpecValidation(t *testing.T) {
})
}
}
func TestPodValidation(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 WithPodValidator(ctx, func(ctx context.Context, wp *Pod) *apis.FieldError {
return nil
})
},
want: nil,
}, {
name: "no busybox",
with: func(ctx context.Context) context.Context {
return WithPodValidator(ctx, func(ctx context.Context, wp *Pod) *apis.FieldError {
for i, c := range wp.Spec.InitContainers {
if c.Image == "busybox" {
return apis.ErrInvalidValue(c.Image, "image").ViaFieldIndex("spec.template.spec.initContainers", i)
}
}
for i, c := range wp.Spec.Containers {
if c.Image == "busybox" {
return apis.ErrInvalidValue(c.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) {
p := Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "blah",
Image: "busybox",
}},
},
}
ctx := test.with(context.Background())
got := p.Validate(ctx)
if test.want.Error() != got.Error() {
t.Errorf("Validate() = %v, wanted %v", got, test.want)
}
})
}
}

View File

@ -367,6 +367,66 @@ func (in *KResourceList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Pod) DeepCopyInto(out *Pod) {
*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 Pod.
func (in *Pod) DeepCopy() *Pod {
if in == nil {
return nil
}
out := new(Pod)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Pod) 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 *PodList) DeepCopyInto(out *PodList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]WithPod, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodList.
func (in *PodList) DeepCopy() *PodList {
if in == nil {
return nil
}
out := new(PodList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PodList) 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 *PodSpecable) DeepCopyInto(out *PodSpecable) {
*out = *in