api: dynamic resource allocation API
This adds a new resource.k8s.io API group with v1alpha1 as version. It contains four new types: resource.ResourceClaim, resource.ResourceClass, resource.ResourceClaimTemplate, and resource.PodScheduling.
This commit is contained in:
parent
5433da0419
commit
5cca60f0b8
@ -283,6 +283,7 @@ var apiVersionPriorities = map[schema.GroupVersion]priority{
|
|||||||
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta1"}: {group: 16100, version: 12},
|
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1beta1"}: {group: 16100, version: 12},
|
||||||
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1alpha1"}: {group: 16100, version: 9},
|
{Group: "flowcontrol.apiserver.k8s.io", Version: "v1alpha1"}: {group: 16100, version: 9},
|
||||||
{Group: "internal.apiserver.k8s.io", Version: "v1alpha1"}: {group: 16000, version: 9},
|
{Group: "internal.apiserver.k8s.io", Version: "v1alpha1"}: {group: 16000, version: 9},
|
||||||
|
{Group: "resource.k8s.io", Version: "v1alpha1"}: {group: 15900, version: 9},
|
||||||
// Append a new group to the end of the list if unsure.
|
// Append a new group to the end of the list if unsure.
|
||||||
// You can use min(existing group)-100 as the initial value for a group.
|
// You can use min(existing group)-100 as the initial value for a group.
|
||||||
// Version can be set to 9 (to have space around) for a new group.
|
// Version can be set to 9 (to have space around) for a new group.
|
||||||
|
@ -89,6 +89,7 @@ coordination.k8s.io/v1beta1 \
|
|||||||
coordination.k8s.io/v1 \
|
coordination.k8s.io/v1 \
|
||||||
discovery.k8s.io/v1 \
|
discovery.k8s.io/v1 \
|
||||||
discovery.k8s.io/v1beta1 \
|
discovery.k8s.io/v1beta1 \
|
||||||
|
resource.k8s.io/v1alpha1 \
|
||||||
extensions/v1beta1 \
|
extensions/v1beta1 \
|
||||||
events.k8s.io/v1 \
|
events.k8s.io/v1 \
|
||||||
events.k8s.io/v1beta1 \
|
events.k8s.io/v1beta1 \
|
||||||
|
@ -139,6 +139,10 @@ func TestDefaulting(t *testing.T) {
|
|||||||
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBindingList"}: {},
|
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBindingList"}: {},
|
||||||
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}: {},
|
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}: {},
|
||||||
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBindingList"}: {},
|
{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBindingList"}: {},
|
||||||
|
{Group: "resource.k8s.io", Version: "v1alpha1", Kind: "ResourceClaim"}: {},
|
||||||
|
{Group: "resource.k8s.io", Version: "v1alpha1", Kind: "ResourceClaimList"}: {},
|
||||||
|
{Group: "resource.k8s.io", Version: "v1alpha1", Kind: "ResourceClaimTemplate"}: {},
|
||||||
|
{Group: "resource.k8s.io", Version: "v1alpha1", Kind: "ResourceClaimTemplateList"}: {},
|
||||||
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicy"}: {},
|
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicy"}: {},
|
||||||
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyList"}: {},
|
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyList"}: {},
|
||||||
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBinding"}: {},
|
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBinding"}: {},
|
||||||
|
@ -42,6 +42,7 @@ import (
|
|||||||
networkingfuzzer "k8s.io/kubernetes/pkg/apis/networking/fuzzer"
|
networkingfuzzer "k8s.io/kubernetes/pkg/apis/networking/fuzzer"
|
||||||
policyfuzzer "k8s.io/kubernetes/pkg/apis/policy/fuzzer"
|
policyfuzzer "k8s.io/kubernetes/pkg/apis/policy/fuzzer"
|
||||||
rbacfuzzer "k8s.io/kubernetes/pkg/apis/rbac/fuzzer"
|
rbacfuzzer "k8s.io/kubernetes/pkg/apis/rbac/fuzzer"
|
||||||
|
resourcefuzzer "k8s.io/kubernetes/pkg/apis/resource/fuzzer"
|
||||||
schedulingfuzzer "k8s.io/kubernetes/pkg/apis/scheduling/fuzzer"
|
schedulingfuzzer "k8s.io/kubernetes/pkg/apis/scheduling/fuzzer"
|
||||||
storagefuzzer "k8s.io/kubernetes/pkg/apis/storage/fuzzer"
|
storagefuzzer "k8s.io/kubernetes/pkg/apis/storage/fuzzer"
|
||||||
)
|
)
|
||||||
@ -101,6 +102,7 @@ var FuzzerFuncs = fuzzer.MergeFuzzerFuncs(
|
|||||||
autoscalingfuzzer.Funcs,
|
autoscalingfuzzer.Funcs,
|
||||||
rbacfuzzer.Funcs,
|
rbacfuzzer.Funcs,
|
||||||
policyfuzzer.Funcs,
|
policyfuzzer.Funcs,
|
||||||
|
resourcefuzzer.Funcs,
|
||||||
certificatesfuzzer.Funcs,
|
certificatesfuzzer.Funcs,
|
||||||
admissionregistrationfuzzer.Funcs,
|
admissionregistrationfuzzer.Funcs,
|
||||||
storagefuzzer.Funcs,
|
storagefuzzer.Funcs,
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
_ "k8s.io/kubernetes/pkg/apis/node/install"
|
_ "k8s.io/kubernetes/pkg/apis/node/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/policy/install"
|
_ "k8s.io/kubernetes/pkg/apis/policy/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
|
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/storage/install"
|
_ "k8s.io/kubernetes/pkg/apis/storage/install"
|
||||||
)
|
)
|
||||||
|
6
pkg/apis/resource/OWNERS
Normal file
6
pkg/apis/resource/OWNERS
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# See the OWNERS docs at https://go.k8s.io/owners
|
||||||
|
|
||||||
|
reviewers:
|
||||||
|
- bart0sh
|
||||||
|
- klueska
|
||||||
|
- pohly
|
21
pkg/apis/resource/doc.go
Normal file
21
pkg/apis/resource/doc.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen=package
|
||||||
|
|
||||||
|
// Package resource contains the latest (or "internal") version of the
|
||||||
|
// Kubernetes resource API objects.
|
||||||
|
package resource // import "k8s.io/kubernetes/pkg/apis/resource"
|
40
pkg/apis/resource/fuzzer/fuzzer.go
Normal file
40
pkg/apis/resource/fuzzer/fuzzer.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 fuzzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
fuzz "github.com/google/gofuzz"
|
||||||
|
|
||||||
|
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Funcs contains the fuzzer functions for the resource group.
|
||||||
|
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||||
|
return []interface{}{
|
||||||
|
func(obj *resource.ResourceClaimSpec, c fuzz.Continue) {
|
||||||
|
c.FuzzNoCustom(obj) // fuzz self without calling this function again
|
||||||
|
|
||||||
|
// Custom fuzzing for allocation mode: pick one valid mode randomly.
|
||||||
|
modes := []resource.AllocationMode{
|
||||||
|
resource.AllocationModeImmediate,
|
||||||
|
resource.AllocationModeWaitForFirstConsumer,
|
||||||
|
}
|
||||||
|
obj.AllocationMode = modes[c.Rand.Intn(len(modes))]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
38
pkg/apis/resource/install/install.go
Normal file
38
pkg/apis/resource/install/install.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 install installs the resource API, making it available as an
|
||||||
|
// option to all of the API encoding/decoding machinery.
|
||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Install(legacyscheme.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install registers the API group and adds types to a scheme
|
||||||
|
func Install(scheme *runtime.Scheme) {
|
||||||
|
utilruntime.Must(resource.AddToScheme(scheme))
|
||||||
|
utilruntime.Must(v1alpha1.AddToScheme(scheme))
|
||||||
|
utilruntime.Must(scheme.SetVersionPriority(v1alpha1.SchemeGroupVersion))
|
||||||
|
}
|
75
pkg/apis/resource/install/install_test.go
Normal file
75
pkg/apis/resource/install/install_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
|
internal "k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResourceVersioner(t *testing.T) {
|
||||||
|
claim := internal.ResourceClaim{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "10"}}
|
||||||
|
version, err := meta.NewAccessor().ResourceVersion(&claim)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if version != "10" {
|
||||||
|
t.Errorf("unexpected version %v", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
claimList := internal.ResourceClaimList{ListMeta: metav1.ListMeta{ResourceVersion: "10"}}
|
||||||
|
version, err = meta.NewAccessor().ResourceVersion(&claimList)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if version != "10" {
|
||||||
|
t.Errorf("unexpected version %v", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodec(t *testing.T) {
|
||||||
|
claim := internal.ResourceClaim{}
|
||||||
|
data, err := runtime.Encode(legacyscheme.Codecs.LegacyCodec(schema.GroupVersion{Group: "resource.k8s.io", Version: "v1alpha1"}), &claim)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
other := internal.ResourceClaim{}
|
||||||
|
if err := json.Unmarshal(data, &other); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if other.APIVersion != "resource.k8s.io/v1alpha1" || other.Kind != "ResourceClaim" {
|
||||||
|
t.Errorf("unexpected unmarshalled object %#v", other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnversioned(t *testing.T) {
|
||||||
|
for _, obj := range []runtime.Object{
|
||||||
|
&metav1.Status{},
|
||||||
|
} {
|
||||||
|
if unversioned, ok := legacyscheme.Scheme.IsUnversioned(obj); !unversioned || !ok {
|
||||||
|
t.Errorf("%v is expected to be unversioned", reflect.TypeOf(obj))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
pkg/apis/resource/register.go
Normal file
66
pkg/apis/resource/register.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroupName is the group name use in this package
|
||||||
|
const GroupName = "resource.k8s.io"
|
||||||
|
|
||||||
|
// SchemeGroupVersion is group version used to register these objects
|
||||||
|
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||||
|
|
||||||
|
// Kind takes an unqualified kind and returns a Group qualified GroupKind
|
||||||
|
func Kind(kind string) schema.GroupKind {
|
||||||
|
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||||
|
func Resource(resource string) schema.GroupResource {
|
||||||
|
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// SchemeBuilder object to register various known types
|
||||||
|
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||||
|
|
||||||
|
// AddToScheme represents a func that can be used to apply all the registered
|
||||||
|
// funcs in a scheme
|
||||||
|
AddToScheme = SchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
|
|
||||||
|
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||||
|
if err := scheme.AddIgnoredConversionType(&metav1.TypeMeta{}, &metav1.TypeMeta{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||||
|
&ResourceClass{},
|
||||||
|
&ResourceClassList{},
|
||||||
|
&ResourceClaim{},
|
||||||
|
&ResourceClaimList{},
|
||||||
|
&ResourceClaimTemplate{},
|
||||||
|
&ResourceClaimTemplateList{},
|
||||||
|
&PodScheduling{},
|
||||||
|
&PodSchedulingList{},
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
404
pkg/apis/resource/types.go
Normal file
404
pkg/apis/resource/types.go
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// ResourceClaim describes which resources are needed by a resource consumer.
|
||||||
|
// Its status tracks whether the resource has been allocated and what the
|
||||||
|
// resulting attributes are.
|
||||||
|
//
|
||||||
|
// This is an alpha type and requires enabling the DynamicResourceAllocation
|
||||||
|
// feature gate.
|
||||||
|
type ResourceClaim struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
// Standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta
|
||||||
|
|
||||||
|
// Spec describes the desired attributes of a resource that then needs
|
||||||
|
// to be allocated. It can only be set once when creating the
|
||||||
|
// ResourceClaim.
|
||||||
|
Spec ResourceClaimSpec
|
||||||
|
|
||||||
|
// Status describes whether the resource is available and with which
|
||||||
|
// attributes.
|
||||||
|
// +optional
|
||||||
|
Status ResourceClaimStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimSpec defines how a resource is to be allocated.
|
||||||
|
type ResourceClaimSpec struct {
|
||||||
|
// ResourceClassName references the driver and additional parameters
|
||||||
|
// via the name of a ResourceClass that was created as part of the
|
||||||
|
// driver deployment.
|
||||||
|
ResourceClassName string
|
||||||
|
|
||||||
|
// ParametersRef references a separate object with arbitrary parameters
|
||||||
|
// that will be used by the driver when allocating a resource for the
|
||||||
|
// claim.
|
||||||
|
//
|
||||||
|
// The object must be in the same namespace as the ResourceClaim.
|
||||||
|
// +optional
|
||||||
|
ParametersRef *ResourceClaimParametersReference
|
||||||
|
|
||||||
|
// Allocation can start immediately or when a Pod wants to use the
|
||||||
|
// resource. "WaitForFirstConsumer" is the default.
|
||||||
|
// +optional
|
||||||
|
AllocationMode AllocationMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllocationMode describes whether a ResourceClaim gets allocated immediately
|
||||||
|
// when it gets created (AllocationModeImmediate) or whether allocation is
|
||||||
|
// delayed until it is needed for a Pod
|
||||||
|
// (AllocationModeWaitForFirstConsumer). Other modes might get added in the
|
||||||
|
// future.
|
||||||
|
type AllocationMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// When a ResourceClaim has AllocationModeWaitForFirstConsumer, allocation is
|
||||||
|
// delayed until a Pod gets scheduled that needs the ResourceClaim. The
|
||||||
|
// scheduler will consider all resource requirements of that Pod and
|
||||||
|
// trigger allocation for a node that fits the Pod.
|
||||||
|
AllocationModeWaitForFirstConsumer AllocationMode = "WaitForFirstConsumer"
|
||||||
|
|
||||||
|
// When a ResourceClaim has AllocationModeImmediate, allocation starts
|
||||||
|
// as soon as the ResourceClaim gets created. This is done without
|
||||||
|
// considering the needs of Pods that will use the ResourceClaim
|
||||||
|
// because those Pods are not known yet.
|
||||||
|
AllocationModeImmediate AllocationMode = "Immediate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceClaimStatus tracks whether the resource has been allocated and what
|
||||||
|
// the resulting attributes are.
|
||||||
|
type ResourceClaimStatus struct {
|
||||||
|
// DriverName is a copy of the driver name from the ResourceClass at
|
||||||
|
// the time when allocation started.
|
||||||
|
// +optional
|
||||||
|
DriverName string
|
||||||
|
|
||||||
|
// Allocation is set by the resource driver once a resource has been
|
||||||
|
// allocated successfully. If this is not specified, the resource is
|
||||||
|
// not yet allocated.
|
||||||
|
// +optional
|
||||||
|
Allocation *AllocationResult
|
||||||
|
|
||||||
|
// ReservedFor indicates which entities are currently allowed to use
|
||||||
|
// the claim. A Pod which references a ResourceClaim which is not
|
||||||
|
// reserved for that Pod will not be started.
|
||||||
|
//
|
||||||
|
// There can be at most 32 such reservations. This may get increased in
|
||||||
|
// the future, but not reduced.
|
||||||
|
// +optional
|
||||||
|
ReservedFor []ResourceClaimConsumerReference
|
||||||
|
|
||||||
|
// DeallocationRequested indicates that a ResourceClaim is to be
|
||||||
|
// deallocated.
|
||||||
|
//
|
||||||
|
// The driver then must deallocate this claim and reset the field
|
||||||
|
// together with clearing the Allocation field.
|
||||||
|
//
|
||||||
|
// While DeallocationRequested is set, no new consumers may be added to
|
||||||
|
// ReservedFor.
|
||||||
|
// +optional
|
||||||
|
DeallocationRequested bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReservedForMaxSize is the maximum number of entries in
|
||||||
|
// claim.status.reservedFor.
|
||||||
|
const ResourceClaimReservedForMaxSize = 32
|
||||||
|
|
||||||
|
// AllocationResult contains attributed of an allocated resource.
|
||||||
|
type AllocationResult struct {
|
||||||
|
// ResourceHandle contains arbitrary data returned by the driver after a
|
||||||
|
// successful allocation. This is opaque for
|
||||||
|
// Kubernetes. Driver documentation may explain to users how to
|
||||||
|
// interpret this data if needed.
|
||||||
|
//
|
||||||
|
// The maximum size of this field is 16KiB. This may get
|
||||||
|
// increased in the future, but not reduced.
|
||||||
|
// +optional
|
||||||
|
ResourceHandle string
|
||||||
|
|
||||||
|
// This field will get set by the resource driver after it has
|
||||||
|
// allocated the resource driver to inform the scheduler where it can
|
||||||
|
// schedule Pods using the ResourceClaim.
|
||||||
|
//
|
||||||
|
// Setting this field is optional. If null, the resource is available
|
||||||
|
// everywhere.
|
||||||
|
// +optional
|
||||||
|
AvailableOnNodes *core.NodeSelector
|
||||||
|
|
||||||
|
// Shareable determines whether the resource supports more
|
||||||
|
// than one consumer at a time.
|
||||||
|
// +optional
|
||||||
|
Shareable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceHandleMaxSize is the maximum size of allocation.resourceHandle.
|
||||||
|
const ResourceHandleMaxSize = 16 * 1024
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// ResourceClaimList is a collection of claims.
|
||||||
|
type ResourceClaimList struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
// Standard list metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ListMeta
|
||||||
|
|
||||||
|
// Items is the list of resource claims.
|
||||||
|
Items []ResourceClaim
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// PodScheduling objects hold information that is needed to schedule
|
||||||
|
// a Pod with ResourceClaims that use "WaitForFirstConsumer" allocation
|
||||||
|
// mode.
|
||||||
|
//
|
||||||
|
// This is an alpha type and requires enabling the DynamicResourceAllocation
|
||||||
|
// feature gate.
|
||||||
|
type PodScheduling struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
// Standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta
|
||||||
|
|
||||||
|
// Spec describes where resources for the Pod are needed.
|
||||||
|
Spec PodSchedulingSpec
|
||||||
|
|
||||||
|
// Status describes where resources for the Pod can be allocated.
|
||||||
|
Status PodSchedulingStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodSchedulingSpec describes where resources for the Pod are needed.
|
||||||
|
type PodSchedulingSpec struct {
|
||||||
|
// SelectedNode is the node for which allocation of ResourceClaims that
|
||||||
|
// are referenced by the Pod and that use "WaitForFirstConsumer"
|
||||||
|
// allocation is to be attempted.
|
||||||
|
SelectedNode string
|
||||||
|
|
||||||
|
// PotentialNodes lists nodes where the Pod might be able to run.
|
||||||
|
//
|
||||||
|
// The size of this field is limited to 128. This is large enough for
|
||||||
|
// many clusters. Larger clusters may need more attempts to find a node
|
||||||
|
// that suits all pending resources. This may get increased in the
|
||||||
|
// future, but not reduced.
|
||||||
|
// +optional
|
||||||
|
PotentialNodes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodSchedulingStatus describes where resources for the Pod can be allocated.
|
||||||
|
type PodSchedulingStatus struct {
|
||||||
|
// ResourceClaims describes resource availability for each
|
||||||
|
// pod.spec.resourceClaim entry where the corresponding ResourceClaim
|
||||||
|
// uses "WaitForFirstConsumer" allocation mode.
|
||||||
|
// +optional
|
||||||
|
ResourceClaims []ResourceClaimSchedulingStatus
|
||||||
|
|
||||||
|
// If there ever is a need to support other kinds of resources
|
||||||
|
// than ResourceClaim, then new fields could get added here
|
||||||
|
// for those other resources.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimSchedulingStatus contains information about one particular
|
||||||
|
// ResourceClaim with "WaitForFirstConsumer" allocation mode.
|
||||||
|
type ResourceClaimSchedulingStatus struct {
|
||||||
|
// Name matches the pod.spec.resourceClaims[*].Name field.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// UnsuitableNodes lists nodes that the ResourceClaim cannot be
|
||||||
|
// allocated for.
|
||||||
|
//
|
||||||
|
// The size of this field is limited to 128, the same as for
|
||||||
|
// PodSchedulingSpec.PotentialNodes. This may get increased in the
|
||||||
|
// future, but not reduced.
|
||||||
|
// +optional
|
||||||
|
UnsuitableNodes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodSchedulingNodeListMaxSize defines the maximum number of entries in the
|
||||||
|
// node lists that are stored in PodScheduling objects. This limit is part
|
||||||
|
// of the API.
|
||||||
|
const PodSchedulingNodeListMaxSize = 128
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// PodSchedulingList is a collection of Pod scheduling objects.
|
||||||
|
type PodSchedulingList struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
// Standard list metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ListMeta
|
||||||
|
|
||||||
|
// Items is the list of PodScheduling objects.
|
||||||
|
Items []PodScheduling
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// ResourceClass is used by administrators to influence how resources
|
||||||
|
// are allocated.
|
||||||
|
//
|
||||||
|
// This is an alpha type and requires enabling the DynamicResourceAllocation
|
||||||
|
// feature gate.
|
||||||
|
type ResourceClass struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
// Standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta
|
||||||
|
|
||||||
|
// DriverName defines the name of the dynamic resource driver that is
|
||||||
|
// used for allocation of a ResourceClaim that uses this class.
|
||||||
|
//
|
||||||
|
// Resource drivers have a unique name in forward domain order
|
||||||
|
// (acme.example.com).
|
||||||
|
DriverName string
|
||||||
|
|
||||||
|
// ParametersRef references an arbitrary separate object that may hold
|
||||||
|
// parameters that will be used by the driver when allocating a
|
||||||
|
// resource that uses this class. A dynamic resource driver can
|
||||||
|
// distinguish between parameters stored here and and those stored in
|
||||||
|
// ResourceClaimSpec.
|
||||||
|
// +optional
|
||||||
|
ParametersRef *ResourceClassParametersReference
|
||||||
|
|
||||||
|
// Only nodes matching the selector will be considered by the scheduler
|
||||||
|
// when trying to find a Node that fits a Pod when that Pod uses
|
||||||
|
// a ResourceClaim that has not been allocated yet.
|
||||||
|
//
|
||||||
|
// Setting this field is optional. If null, all nodes are candidates.
|
||||||
|
// +optional
|
||||||
|
SuitableNodes *core.NodeSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// ResourceClassList is a collection of classes.
|
||||||
|
type ResourceClassList struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
// Standard list metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ListMeta
|
||||||
|
|
||||||
|
// Items is the list of resource classes.
|
||||||
|
Items []ResourceClass
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClassParametersReference contains enough information to let you
|
||||||
|
// locate the parameters for a ResourceClass.
|
||||||
|
type ResourceClassParametersReference struct {
|
||||||
|
// APIGroup is the group for the resource being referenced. It is
|
||||||
|
// empty for the core API. This matches the group in the APIVersion
|
||||||
|
// that is used when creating the resources.
|
||||||
|
// +optional
|
||||||
|
APIGroup string
|
||||||
|
// Kind is the type of resource being referenced. This is the same
|
||||||
|
// value as in the parameter object's metadata.
|
||||||
|
Kind string
|
||||||
|
// Name is the name of resource being referenced.
|
||||||
|
Name string
|
||||||
|
// Namespace that contains the referenced resource. Must be empty
|
||||||
|
// for cluster-scoped resources and non-empty for namespaced
|
||||||
|
// resources.
|
||||||
|
// +optional
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimParametersReference contains enough information to let you
|
||||||
|
// locate the parameters for a ResourceClaim. The object must be in the same
|
||||||
|
// namespace as the ResourceClaim.
|
||||||
|
type ResourceClaimParametersReference struct {
|
||||||
|
// APIGroup is the group for the resource being referenced. It is
|
||||||
|
// empty for the core API. This matches the group in the APIVersion
|
||||||
|
// that is used when creating the resources.
|
||||||
|
// +optional
|
||||||
|
APIGroup string
|
||||||
|
// Kind is the type of resource being referenced. This is the same
|
||||||
|
// value as in the parameter object's metadata, for example "ConfigMap".
|
||||||
|
Kind string
|
||||||
|
// Name is the name of resource being referenced.
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimConsumerReference contains enough information to let you
|
||||||
|
// locate the consumer of a ResourceClaim. The user must be a resource in the same
|
||||||
|
// namespace as the ResourceClaim.
|
||||||
|
type ResourceClaimConsumerReference struct {
|
||||||
|
// APIGroup is the group for the resource being referenced. It is
|
||||||
|
// empty for the core API. This matches the group in the APIVersion
|
||||||
|
// that is used when creating the resources.
|
||||||
|
// +optional
|
||||||
|
APIGroup string
|
||||||
|
// Resource is the type of resource being referenced, for example "pods".
|
||||||
|
Resource string
|
||||||
|
// Name is the name of resource being referenced.
|
||||||
|
Name string
|
||||||
|
// UID identifies exactly one incarnation of the resource.
|
||||||
|
UID types.UID
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// ResourceClaimTemplate is used to produce ResourceClaim objects.
|
||||||
|
type ResourceClaimTemplate struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
// Standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta
|
||||||
|
|
||||||
|
// Describes the ResourceClaim that is to be generated.
|
||||||
|
//
|
||||||
|
// This field is immutable. A ResourceClaim will get created by the
|
||||||
|
// control plane for a Pod when needed and then not get updated
|
||||||
|
// anymore.
|
||||||
|
Spec ResourceClaimTemplateSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimTemplateSpec contains the metadata and fields for a ResourceClaim.
|
||||||
|
type ResourceClaimTemplateSpec struct {
|
||||||
|
// ObjectMeta may contain labels and annotations that will be copied into the PVC
|
||||||
|
// when creating it. No other fields are allowed and will be rejected during
|
||||||
|
// validation.
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta
|
||||||
|
|
||||||
|
// Spec for the ResourceClaim. The entire content is copied unchanged
|
||||||
|
// into the ResourceClaim that gets created from this template. The
|
||||||
|
// same fields as in a ResourceClaim are also valid here.
|
||||||
|
Spec ResourceClaimSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
// ResourceClaimTemplateList is a collection of claim templates.
|
||||||
|
type ResourceClaimTemplateList struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
// Standard list metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ListMeta
|
||||||
|
|
||||||
|
// Items is the list of resource claim templates.
|
||||||
|
Items []ResourceClaimTemplate
|
||||||
|
}
|
25
pkg/apis/resource/v1alpha1/conversion.go
Normal file
25
pkg/apis/resource/v1alpha1/conversion.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addConversionFuncs(scheme *runtime.Scheme) error {
|
||||||
|
return nil
|
||||||
|
}
|
32
pkg/apis/resource/v1alpha1/defaults.go
Normal file
32
pkg/apis/resource/v1alpha1/defaults.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/api/resource/v1alpha1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||||
|
return RegisterDefaults(scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDefaults_ResourceClaimSpec(obj *v1alpha1.ResourceClaimSpec) {
|
||||||
|
if obj.AllocationMode == "" {
|
||||||
|
obj.AllocationMode = v1alpha1.AllocationModeWaitForFirstConsumer
|
||||||
|
}
|
||||||
|
}
|
75
pkg/apis/resource/v1alpha1/defaults_test.go
Normal file
75
pkg/apis/resource/v1alpha1/defaults_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 v1alpha1_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
v1alpha1 "k8s.io/api/resource/v1alpha1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
||||||
|
// ensure types are installed
|
||||||
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetDefaultAllocationMode(t *testing.T) {
|
||||||
|
claim := &v1alpha1.ResourceClaim{}
|
||||||
|
|
||||||
|
// field should be defaulted
|
||||||
|
defaultMode := v1alpha1.AllocationModeWaitForFirstConsumer
|
||||||
|
output := roundTrip(t, runtime.Object(claim)).(*v1alpha1.ResourceClaim)
|
||||||
|
outMode := output.Spec.AllocationMode
|
||||||
|
if outMode != defaultMode {
|
||||||
|
t.Errorf("Expected AllocationMode to be defaulted to: %+v, got: %+v", defaultMode, outMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// field should not change
|
||||||
|
nonDefaultMode := v1alpha1.AllocationModeImmediate
|
||||||
|
claim = &v1alpha1.ResourceClaim{
|
||||||
|
Spec: v1alpha1.ResourceClaimSpec{
|
||||||
|
AllocationMode: nonDefaultMode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
output = roundTrip(t, runtime.Object(claim)).(*v1alpha1.ResourceClaim)
|
||||||
|
outMode = output.Spec.AllocationMode
|
||||||
|
if outMode != v1alpha1.AllocationModeImmediate {
|
||||||
|
t.Errorf("Expected AllocationMode to remain %+v, got: %+v", nonDefaultMode, outMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
|
||||||
|
codec := legacyscheme.Codecs.LegacyCodec(v1alpha1.SchemeGroupVersion)
|
||||||
|
data, err := runtime.Encode(codec, obj)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v\n %#v", err, obj)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
obj2, err := runtime.Decode(codec, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object)
|
||||||
|
err = legacyscheme.Scheme.Convert(obj2, obj3, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v\nSource: %#v", err, obj2)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return obj3
|
||||||
|
}
|
23
pkg/apis/resource/v1alpha1/doc.go
Normal file
23
pkg/apis/resource/v1alpha1/doc.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/resource
|
||||||
|
// +k8s:conversion-gen-external-types=k8s.io/api/resource/v1alpha1
|
||||||
|
// +k8s:defaulter-gen=TypeMeta
|
||||||
|
// +k8s:defaulter-gen-input=k8s.io/api/resource/v1alpha1
|
||||||
|
|
||||||
|
// Package v1alpha1 is the v1alpha1 version of the resource API.
|
||||||
|
package v1alpha1 // import "k8s.io/kubernetes/pkg/apis/resource/v1alpha1"
|
46
pkg/apis/resource/v1alpha1/register.go
Normal file
46
pkg/apis/resource/v1alpha1/register.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/api/resource/v1alpha1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
localSchemeBuilder = &v1alpha1.SchemeBuilder
|
||||||
|
AddToScheme = localSchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// We only register manually written functions here. The registration of the
|
||||||
|
// generated functions takes place in the generated files. The separation
|
||||||
|
// makes the code compile even when the generated files are missing.
|
||||||
|
localSchemeBuilder.Register(addDefaultingFuncs, addConversionFuncs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove these global variables
|
||||||
|
// GroupName is the group name use in this package
|
||||||
|
const GroupName = "resource.k8s.io"
|
||||||
|
|
||||||
|
// SchemeGroupVersion is group version used to register these objects
|
||||||
|
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||||
|
|
||||||
|
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||||
|
func Resource(resource string) schema.GroupResource {
|
||||||
|
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||||
|
}
|
317
pkg/apis/resource/validation/validation.go
Normal file
317
pkg/apis/resource/validation/validation.go
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validateResourceClaimName can be used to check whether the given
|
||||||
|
// name for a ResourceClaim is valid.
|
||||||
|
var validateResourceClaimName = apimachineryvalidation.NameIsDNSSubdomain
|
||||||
|
|
||||||
|
// validateResourceClaimTemplateName can be used to check whether the given
|
||||||
|
// name for a ResourceClaimTemplate is valid.
|
||||||
|
var validateResourceClaimTemplateName = apimachineryvalidation.NameIsDNSSubdomain
|
||||||
|
|
||||||
|
// validateResourceDriverName reuses the validation of a CSI driver because
|
||||||
|
// the allowed values are exactly the same.
|
||||||
|
var validateResourceDriverName = corevalidation.ValidateCSIDriverName
|
||||||
|
|
||||||
|
// ValidateClaim validates a ResourceClaim.
|
||||||
|
func ValidateClaim(resourceClaim *resource.ResourceClaim) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMeta(&resourceClaim.ObjectMeta, true, validateResourceClaimName, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, validateResourceClaimSpec(&resourceClaim.Spec, field.NewPath("spec"))...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateResourceClaimSpec(spec *resource.ResourceClaimSpec, fldPath *field.Path) field.ErrorList {
|
||||||
|
allErrs := field.ErrorList{}
|
||||||
|
for _, msg := range corevalidation.ValidateClassName(spec.ResourceClassName, false) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceClassName"), spec.ResourceClassName, msg))
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, validateResourceClaimParameters(spec.ParametersRef, fldPath.Child("parametersRef"))...)
|
||||||
|
if !supportedAllocationModes.Has(string(spec.AllocationMode)) {
|
||||||
|
allErrs = append(allErrs, field.NotSupported(fldPath.Child("allocationMode"), spec.AllocationMode, supportedAllocationModes.List()))
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportedAllocationModes = sets.NewString(string(resource.AllocationModeImmediate), string(resource.AllocationModeWaitForFirstConsumer))
|
||||||
|
|
||||||
|
// It would have been nice to use Go generics to reuse the same validation
|
||||||
|
// function for Kind and Name in both types, but generics cannot be used to
|
||||||
|
// access common fields in structs.
|
||||||
|
|
||||||
|
func validateResourceClaimParameters(ref *resource.ResourceClaimParametersReference, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
if ref != nil {
|
||||||
|
if ref.Kind == "" {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("kind"), ""))
|
||||||
|
}
|
||||||
|
if ref.Name == "" {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateClassParameters(ref *resource.ResourceClassParametersReference, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
if ref != nil {
|
||||||
|
if ref.Kind == "" {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("kind"), ""))
|
||||||
|
}
|
||||||
|
if ref.Name == "" {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
|
||||||
|
}
|
||||||
|
if ref.Namespace != "" {
|
||||||
|
for _, msg := range apimachineryvalidation.ValidateNamespaceName(ref.Namespace, false) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("namespace"), ref.Namespace, msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateClass validates a ResourceClass.
|
||||||
|
func ValidateClass(resourceClass *resource.ResourceClass) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMeta(&resourceClass.ObjectMeta, false, corevalidation.ValidateClassName, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, validateResourceDriverName(resourceClass.DriverName, field.NewPath("driverName"))...)
|
||||||
|
allErrs = append(allErrs, validateClassParameters(resourceClass.ParametersRef, field.NewPath("parametersRef"))...)
|
||||||
|
if resourceClass.SuitableNodes != nil {
|
||||||
|
allErrs = append(allErrs, corevalidation.ValidateNodeSelector(resourceClass.SuitableNodes, field.NewPath("suitableNodes"))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateClassUpdate tests if an update to ResourceClass is valid.
|
||||||
|
func ValidateClassUpdate(resourceClass, oldClass *resource.ResourceClass) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClass.ObjectMeta, &oldClass.ObjectMeta, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, ValidateClass(resourceClass)...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateClaimUpdate tests if an update to ResourceClaim is valid.
|
||||||
|
func ValidateClaimUpdate(resourceClaim, oldClaim *resource.ResourceClaim) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(resourceClaim.Spec, oldClaim.Spec, field.NewPath("spec"))...)
|
||||||
|
allErrs = append(allErrs, ValidateClaim(resourceClaim)...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateClaimStatusUpdate tests if an update to the status of a ResourceClaim is valid.
|
||||||
|
func ValidateClaimStatusUpdate(resourceClaim, oldClaim *resource.ResourceClaim) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldClaim.ObjectMeta, field.NewPath("metadata"))
|
||||||
|
fldPath := field.NewPath("status")
|
||||||
|
// The name might not be set yet.
|
||||||
|
if resourceClaim.Status.DriverName != "" {
|
||||||
|
allErrs = append(allErrs, validateResourceDriverName(resourceClaim.Status.DriverName, fldPath.Child("driverName"))...)
|
||||||
|
} else if resourceClaim.Status.Allocation != nil {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("driverName"), "must be specified when `allocation` is set"))
|
||||||
|
}
|
||||||
|
|
||||||
|
allErrs = append(allErrs, validateAllocationResult(resourceClaim.Status.Allocation, fldPath.Child("allocation"))...)
|
||||||
|
allErrs = append(allErrs, validateSliceIsASet(resourceClaim.Status.ReservedFor, resource.ResourceClaimReservedForMaxSize,
|
||||||
|
validateResourceClaimUserReference, fldPath.Child("reservedFor"))...)
|
||||||
|
|
||||||
|
// Now check for invariants that must be valid for a ResourceClaim.
|
||||||
|
if len(resourceClaim.Status.ReservedFor) > 0 {
|
||||||
|
if resourceClaim.Status.Allocation == nil {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "may not be specified when `allocated` is not set"))
|
||||||
|
} else {
|
||||||
|
if !resourceClaim.Status.Allocation.Shareable && len(resourceClaim.Status.ReservedFor) > 1 {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "may not be reserved more than once"))
|
||||||
|
}
|
||||||
|
// Items may be removed from ReservedFor while the claim is meant to be deallocated,
|
||||||
|
// but not added.
|
||||||
|
if resourceClaim.DeletionTimestamp != nil || resourceClaim.Status.DeallocationRequested {
|
||||||
|
oldSet := sets.New(oldClaim.Status.ReservedFor...)
|
||||||
|
newSet := sets.New(resourceClaim.Status.ReservedFor...)
|
||||||
|
newItems := newSet.Difference(oldSet)
|
||||||
|
if len(newItems) > 0 {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !oldClaim.Status.DeallocationRequested &&
|
||||||
|
resourceClaim.Status.DeallocationRequested &&
|
||||||
|
len(resourceClaim.Status.ReservedFor) > 0 {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("deallocationRequested"), "deallocation cannot be requested while `reservedFor` is set"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceClaim.Status.Allocation == nil &&
|
||||||
|
resourceClaim.Status.DeallocationRequested {
|
||||||
|
// Either one or the other field was modified incorrectly.
|
||||||
|
// For the sake of simplicity this only reports the invalid
|
||||||
|
// end result.
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath, "`allocation` must be set when `deallocationRequested` is set"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once deallocation has been requested, that request cannot be removed
|
||||||
|
// anymore because the deallocation may already have started. The field
|
||||||
|
// can only get reset by the driver together with removing the
|
||||||
|
// allocation.
|
||||||
|
if oldClaim.Status.DeallocationRequested &&
|
||||||
|
!resourceClaim.Status.DeallocationRequested &&
|
||||||
|
resourceClaim.Status.Allocation != nil {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("deallocationRequested"), "may not be cleared when `allocation` is set"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAllocationResult(allocation *resource.AllocationResult, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
if allocation != nil {
|
||||||
|
if len(allocation.ResourceHandle) > resource.ResourceHandleMaxSize {
|
||||||
|
allErrs = append(allErrs, field.TooLongMaxLength(fldPath.Child("resourceHandle"), len(allocation.ResourceHandle), resource.ResourceHandleMaxSize))
|
||||||
|
}
|
||||||
|
if allocation.AvailableOnNodes != nil {
|
||||||
|
allErrs = append(allErrs, corevalidation.ValidateNodeSelector(allocation.AvailableOnNodes, fldPath.Child("availableOnNodes"))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateResourceClaimUserReference(ref resource.ResourceClaimConsumerReference, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
if ref.Resource == "" {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("resource"), ""))
|
||||||
|
}
|
||||||
|
if ref.Name == "" {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
|
||||||
|
}
|
||||||
|
if ref.UID == "" {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("uid"), ""))
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateSliceIsASet ensures that a slice contains no duplicates and does not exceed a certain maximum size.
|
||||||
|
func validateSliceIsASet[T comparable](slice []T, maxSize int, validateItem func(item T, fldPath *field.Path) field.ErrorList, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
allItems := sets.New[T]()
|
||||||
|
for i, item := range slice {
|
||||||
|
idxPath := fldPath.Index(i)
|
||||||
|
if allItems.Has(item) {
|
||||||
|
allErrs = append(allErrs, field.Duplicate(idxPath, item))
|
||||||
|
} else {
|
||||||
|
allErrs = append(allErrs, validateItem(item, idxPath)...)
|
||||||
|
allItems.Insert(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(slice) > maxSize {
|
||||||
|
// Dumping the entire field into the error message is likely to be too long,
|
||||||
|
// in particular when it is already beyond the maximum size. Instead this
|
||||||
|
// just shows the number of entries.
|
||||||
|
allErrs = append(allErrs, field.TooLongMaxLength(fldPath, len(slice), maxSize))
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePodScheduling validates a PodScheduling.
|
||||||
|
func ValidatePodScheduling(resourceClaim *resource.PodScheduling) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMeta(&resourceClaim.ObjectMeta, true, corevalidation.ValidatePodName, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, validatePodSchedulingSpec(&resourceClaim.Spec, field.NewPath("spec"))...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePodSchedulingSpec(spec *resource.PodSchedulingSpec, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
// Checking PotentialNodes for duplicates is intentionally not done. It
|
||||||
|
// could be fairly expensive and the only component which normally has
|
||||||
|
// permissions to set this field, kube-scheduler, is a trusted
|
||||||
|
// component. Also, if it gets this wrong because of a bug, then the
|
||||||
|
// effect is limited (same semantic).
|
||||||
|
if len(spec.PotentialNodes) > resource.PodSchedulingNodeListMaxSize {
|
||||||
|
allErrs = append(allErrs, field.TooLongMaxLength(fldPath.Child("potentialNodes"), nil, resource.PodSchedulingNodeListMaxSize))
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePodSchedulingUpdate tests if an update to PodScheduling is valid.
|
||||||
|
func ValidatePodSchedulingUpdate(resourceClaim, oldPodScheduling *resource.PodScheduling) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldPodScheduling.ObjectMeta, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, ValidatePodScheduling(resourceClaim)...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePodSchedulingStatusUpdate tests if an update to the status of a PodScheduling is valid.
|
||||||
|
func ValidatePodSchedulingStatusUpdate(resourceClaim, oldPodScheduling *resource.PodScheduling) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&resourceClaim.ObjectMeta, &oldPodScheduling.ObjectMeta, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, validatePodSchedulingStatus(&resourceClaim.Status, field.NewPath("status"))...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePodSchedulingStatus(status *resource.PodSchedulingStatus, fldPath *field.Path) field.ErrorList {
|
||||||
|
return validatePodSchedulingClaims(status.ResourceClaims, fldPath.Child("claims"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePodSchedulingClaims(claimStatuses []resource.ResourceClaimSchedulingStatus, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
names := sets.NewString()
|
||||||
|
for i, claimStatus := range claimStatuses {
|
||||||
|
allErrs = append(allErrs, validatePodSchedulingClaim(claimStatus, fldPath.Index(i))...)
|
||||||
|
if names.Has(claimStatus.Name) {
|
||||||
|
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), claimStatus.Name))
|
||||||
|
} else {
|
||||||
|
names.Insert(claimStatus.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePodSchedulingClaim(claim resource.ResourceClaimSchedulingStatus, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
// Checking UnsuitableNodes for duplicates is intentionally not done. It
|
||||||
|
// could be fairly expensive and if a resource driver gets this wrong,
|
||||||
|
// then it is only going to have a negative effect for the pods relying
|
||||||
|
// on this driver.
|
||||||
|
if len(claim.UnsuitableNodes) > resource.PodSchedulingNodeListMaxSize {
|
||||||
|
allErrs = append(allErrs, field.TooLongMaxLength(fldPath.Child("unsuitableNodes"), nil, resource.PodSchedulingNodeListMaxSize))
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateClaimTemplace validates a ResourceClaimTemplate.
|
||||||
|
func ValidateClaimTemplate(template *resource.ResourceClaimTemplate) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMeta(&template.ObjectMeta, true, validateResourceClaimTemplateName, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, validateResourceClaimTemplateSpec(&template.Spec, field.NewPath("spec"))...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateResourceClaimTemplateSpec(spec *resource.ResourceClaimTemplateSpec, fldPath *field.Path) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateTemplateObjectMeta(&spec.ObjectMeta, fldPath.Child("metadata"))
|
||||||
|
allErrs = append(allErrs, validateResourceClaimSpec(&spec.Spec, fldPath.Child("spec"))...)
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateClaimTemplateUpdate tests if an update to template is valid.
|
||||||
|
func ValidateClaimTemplateUpdate(template, oldTemplate *resource.ResourceClaimTemplate) field.ErrorList {
|
||||||
|
allErrs := corevalidation.ValidateObjectMetaUpdate(&template.ObjectMeta, &oldTemplate.ObjectMeta, field.NewPath("metadata"))
|
||||||
|
allErrs = append(allErrs, apimachineryvalidation.ValidateImmutableField(template.Spec, oldTemplate.Spec, field.NewPath("spec"))...)
|
||||||
|
allErrs = append(allErrs, ValidateClaimTemplate(template)...)
|
||||||
|
return allErrs
|
||||||
|
}
|
312
pkg/apis/resource/validation/validation_podscheduling_test.go
Normal file
312
pkg/apis/resource/validation/validation_podscheduling_test.go
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testPodScheduling(name, namespace string, spec resource.PodSchedulingSpec) *resource.PodScheduling {
|
||||||
|
return &resource.PodScheduling{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: spec,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatePodScheduling(t *testing.T) {
|
||||||
|
goodName := "foo"
|
||||||
|
goodNS := "ns"
|
||||||
|
goodPodSchedulingSpec := resource.PodSchedulingSpec{}
|
||||||
|
now := metav1.Now()
|
||||||
|
badName := "!@#$%^"
|
||||||
|
badValue := "spaces not allowed"
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
scheduling *resource.PodScheduling
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"good-scheduling": {
|
||||||
|
scheduling: testPodScheduling(goodName, goodNS, goodPodSchedulingSpec),
|
||||||
|
},
|
||||||
|
"missing-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||||
|
scheduling: testPodScheduling("", goodNS, goodPodSchedulingSpec),
|
||||||
|
},
|
||||||
|
"bad-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
|
scheduling: testPodScheduling(badName, goodNS, goodPodSchedulingSpec),
|
||||||
|
},
|
||||||
|
"missing-namespace": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
|
||||||
|
scheduling: testPodScheduling(goodName, "", goodPodSchedulingSpec),
|
||||||
|
},
|
||||||
|
"generate-name": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.GenerateName = "pvc-"
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"resource-version": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.ResourceVersion = "1"
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.Generation = 100
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"creation-timestamp": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.CreationTimestamp = now
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"deletion-grace-period-seconds": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.DeletionGracePeriodSeconds = pointer.Int64(10)
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"owner-references": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.OwnerReferences = []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: "v1",
|
||||||
|
Kind: "pod",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"finalizers": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.Finalizers = []string{
|
||||||
|
"example.com/foo",
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"managed-fields": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||||
|
{
|
||||||
|
FieldsType: "FieldsV1",
|
||||||
|
Operation: "Apply",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
Manager: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-labels": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.Labels = map[string]string{
|
||||||
|
"apps.kubernetes.io/name": "test",
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-labels": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.Labels = map[string]string{
|
||||||
|
"hello-world": badValue,
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-annotations": {
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.Annotations = map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-annotations": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||||
|
scheduling: func() *resource.PodScheduling {
|
||||||
|
scheduling := testPodScheduling(goodName, goodNS, goodPodSchedulingSpec)
|
||||||
|
scheduling.Annotations = map[string]string{
|
||||||
|
badName: "hello world",
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
errs := ValidatePodScheduling(scenario.scheduling)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatePodSchedulingUpdate(t *testing.T) {
|
||||||
|
validScheduling := testPodScheduling("foo", "ns", resource.PodSchedulingSpec{})
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
oldScheduling *resource.PodScheduling
|
||||||
|
update func(scheduling *resource.PodScheduling) *resource.PodScheduling
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"valid-no-op-update": {
|
||||||
|
oldScheduling: validScheduling,
|
||||||
|
update: func(scheduling *resource.PodScheduling) *resource.PodScheduling { return scheduling },
|
||||||
|
},
|
||||||
|
"add-selected-node": {
|
||||||
|
oldScheduling: validScheduling,
|
||||||
|
update: func(scheduling *resource.PodScheduling) *resource.PodScheduling {
|
||||||
|
scheduling.Spec.SelectedNode = "worker1"
|
||||||
|
return scheduling
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"add-potential-nodes": {
|
||||||
|
oldScheduling: validScheduling,
|
||||||
|
update: func(scheduling *resource.PodScheduling) *resource.PodScheduling {
|
||||||
|
for i := 0; i < resource.PodSchedulingNodeListMaxSize; i++ {
|
||||||
|
scheduling.Spec.PotentialNodes = append(scheduling.Spec.PotentialNodes, fmt.Sprintf("worker%d", i))
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-potential-nodes": {
|
||||||
|
wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("spec", "potentialNodes"), nil, resource.PodSchedulingNodeListMaxSize)},
|
||||||
|
oldScheduling: validScheduling,
|
||||||
|
update: func(scheduling *resource.PodScheduling) *resource.PodScheduling {
|
||||||
|
for i := 0; i < resource.PodSchedulingNodeListMaxSize+1; i++ {
|
||||||
|
scheduling.Spec.PotentialNodes = append(scheduling.Spec.PotentialNodes, fmt.Sprintf("worker%d", i))
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario.oldScheduling.ResourceVersion = "1"
|
||||||
|
errs := ValidatePodSchedulingUpdate(scenario.update(scenario.oldScheduling.DeepCopy()), scenario.oldScheduling)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatePodSchedulingStatusUpdate(t *testing.T) {
|
||||||
|
validScheduling := testPodScheduling("foo", "ns", resource.PodSchedulingSpec{})
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
oldScheduling *resource.PodScheduling
|
||||||
|
update func(scheduling *resource.PodScheduling) *resource.PodScheduling
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"valid-no-op-update": {
|
||||||
|
oldScheduling: validScheduling,
|
||||||
|
update: func(scheduling *resource.PodScheduling) *resource.PodScheduling { return scheduling },
|
||||||
|
},
|
||||||
|
"add-claim-status": {
|
||||||
|
oldScheduling: validScheduling,
|
||||||
|
update: func(scheduling *resource.PodScheduling) *resource.PodScheduling {
|
||||||
|
scheduling.Status.ResourceClaims = append(scheduling.Status.ResourceClaims,
|
||||||
|
resource.ResourceClaimSchedulingStatus{
|
||||||
|
Name: "my-claim",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for i := 0; i < resource.PodSchedulingNodeListMaxSize; i++ {
|
||||||
|
scheduling.Status.ResourceClaims[0].UnsuitableNodes = append(
|
||||||
|
scheduling.Status.ResourceClaims[0].UnsuitableNodes,
|
||||||
|
fmt.Sprintf("worker%d", i),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-duplicated-claim-status": {
|
||||||
|
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("status", "claims").Index(1), "my-claim")},
|
||||||
|
oldScheduling: validScheduling,
|
||||||
|
update: func(scheduling *resource.PodScheduling) *resource.PodScheduling {
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
scheduling.Status.ResourceClaims = append(scheduling.Status.ResourceClaims,
|
||||||
|
resource.ResourceClaimSchedulingStatus{Name: "my-claim"},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-too-long-claim-status": {
|
||||||
|
wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "claims").Index(0).Child("unsuitableNodes"), nil, resource.PodSchedulingNodeListMaxSize)},
|
||||||
|
oldScheduling: validScheduling,
|
||||||
|
update: func(scheduling *resource.PodScheduling) *resource.PodScheduling {
|
||||||
|
scheduling.Status.ResourceClaims = append(scheduling.Status.ResourceClaims,
|
||||||
|
resource.ResourceClaimSchedulingStatus{
|
||||||
|
Name: "my-claim",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for i := 0; i < resource.PodSchedulingNodeListMaxSize+1; i++ {
|
||||||
|
scheduling.Status.ResourceClaims[0].UnsuitableNodes = append(
|
||||||
|
scheduling.Status.ResourceClaims[0].UnsuitableNodes,
|
||||||
|
fmt.Sprintf("worker%d", i),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario.oldScheduling.ResourceVersion = "1"
|
||||||
|
errs := ValidatePodSchedulingStatusUpdate(scenario.update(scenario.oldScheduling.DeepCopy()), scenario.oldScheduling)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
629
pkg/apis/resource/validation/validation_resourceclaim_test.go
Normal file
629
pkg/apis/resource/validation/validation_resourceclaim_test.go
Normal file
@ -0,0 +1,629 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/core"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testClaim(name, namespace string, spec resource.ResourceClaimSpec) *resource.ResourceClaim {
|
||||||
|
return &resource.ResourceClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: spec,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateClaim(t *testing.T) {
|
||||||
|
validMode := resource.AllocationModeImmediate
|
||||||
|
invalidMode := resource.AllocationMode("invalid")
|
||||||
|
goodName := "foo"
|
||||||
|
badName := "!@#$%^"
|
||||||
|
goodNS := "ns"
|
||||||
|
goodClaimSpec := resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: goodName,
|
||||||
|
AllocationMode: validMode,
|
||||||
|
}
|
||||||
|
now := metav1.Now()
|
||||||
|
badValue := "spaces not allowed"
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
claim *resource.ResourceClaim
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"good-claim": {
|
||||||
|
claim: testClaim(goodName, goodNS, goodClaimSpec),
|
||||||
|
},
|
||||||
|
"missing-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||||
|
claim: testClaim("", goodNS, goodClaimSpec),
|
||||||
|
},
|
||||||
|
"bad-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
|
claim: testClaim(badName, goodNS, goodClaimSpec),
|
||||||
|
},
|
||||||
|
"missing-namespace": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
|
||||||
|
claim: testClaim(goodName, "", goodClaimSpec),
|
||||||
|
},
|
||||||
|
"generate-name": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.GenerateName = "pvc-"
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"resource-version": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.ResourceVersion = "1"
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Generation = 100
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"creation-timestamp": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.CreationTimestamp = now
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"deletion-grace-period-seconds": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.DeletionGracePeriodSeconds = pointer.Int64(10)
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"owner-references": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.OwnerReferences = []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: "v1",
|
||||||
|
Kind: "pod",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"finalizers": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Finalizers = []string{
|
||||||
|
"example.com/foo",
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"managed-fields": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||||
|
{
|
||||||
|
FieldsType: "FieldsV1",
|
||||||
|
Operation: "Apply",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
Manager: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-labels": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Labels = map[string]string{
|
||||||
|
"apps.kubernetes.io/name": "test",
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-labels": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Labels = map[string]string{
|
||||||
|
"hello-world": badValue,
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-annotations": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Annotations = map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-annotations": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Annotations = map[string]string{
|
||||||
|
badName: "hello world",
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-classname": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClassName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Spec.ResourceClassName = badName
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-mode": {
|
||||||
|
wantFailures: field.ErrorList{field.NotSupported(field.NewPath("spec", "allocationMode"), invalidMode, supportedAllocationModes.List())},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Spec.AllocationMode = invalidMode
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-parameters": {
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
|
||||||
|
Kind: "foo",
|
||||||
|
Name: "bar",
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"missing-parameters-kind": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "parametersRef", "kind"), "")},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
|
||||||
|
Name: "bar",
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"missing-parameters-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "parametersRef", "name"), "")},
|
||||||
|
claim: func() *resource.ResourceClaim {
|
||||||
|
claim := testClaim(goodName, goodNS, goodClaimSpec)
|
||||||
|
claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
|
||||||
|
Kind: "foo",
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
errs := ValidateClaim(scenario.claim)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateClaimUpdate(t *testing.T) {
|
||||||
|
name := "valid"
|
||||||
|
parameters := &resource.ResourceClaimParametersReference{
|
||||||
|
Kind: "foo",
|
||||||
|
Name: "bar",
|
||||||
|
}
|
||||||
|
validClaim := testClaim("foo", "ns", resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: name,
|
||||||
|
AllocationMode: resource.AllocationModeImmediate,
|
||||||
|
ParametersRef: parameters,
|
||||||
|
})
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
oldClaim *resource.ResourceClaim
|
||||||
|
update func(claim *resource.ResourceClaim) *resource.ResourceClaim
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"valid-no-op-update": {
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { return claim },
|
||||||
|
},
|
||||||
|
"invalid-update-class": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec {
|
||||||
|
spec := validClaim.Spec.DeepCopy()
|
||||||
|
spec.ResourceClassName += "2"
|
||||||
|
return *spec
|
||||||
|
}(), "field is immutable")},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Spec.ResourceClassName += "2"
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-update-remove-parameters": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec {
|
||||||
|
spec := validClaim.Spec.DeepCopy()
|
||||||
|
spec.ParametersRef = nil
|
||||||
|
return *spec
|
||||||
|
}(), "field is immutable")},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Spec.ParametersRef = nil
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-update-mode": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec {
|
||||||
|
spec := validClaim.Spec.DeepCopy()
|
||||||
|
spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
|
||||||
|
return *spec
|
||||||
|
}(), "field is immutable")},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario.oldClaim.ResourceVersion = "1"
|
||||||
|
errs := ValidateClaimUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateClaimStatusUpdate(t *testing.T) {
|
||||||
|
validClaim := testClaim("foo", "ns", resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: "valid",
|
||||||
|
AllocationMode: resource.AllocationModeImmediate,
|
||||||
|
})
|
||||||
|
|
||||||
|
validAllocatedClaim := validClaim.DeepCopy()
|
||||||
|
validAllocatedClaim.Status = resource.ResourceClaimStatus{
|
||||||
|
DriverName: "valid",
|
||||||
|
Allocation: &resource.AllocationResult{
|
||||||
|
ResourceHandle: strings.Repeat(" ", resource.ResourceHandleMaxSize),
|
||||||
|
Shareable: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
oldClaim *resource.ResourceClaim
|
||||||
|
update func(claim *resource.ResourceClaim) *resource.ResourceClaim
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"valid-no-op-update": {
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { return claim },
|
||||||
|
},
|
||||||
|
"add-driver": {
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DriverName = "valid"
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-add-allocation": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("status", "driverName"), "must be specified when `allocation` is set")},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
// DriverName must also get set here!
|
||||||
|
claim.Status.Allocation = &resource.AllocationResult{}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"valid-add-allocation": {
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DriverName = "valid"
|
||||||
|
claim.Status.Allocation = &resource.AllocationResult{
|
||||||
|
ResourceHandle: strings.Repeat(" ", resource.ResourceHandleMaxSize),
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-allocation-handle": {
|
||||||
|
wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "allocation", "resourceHandle"), resource.ResourceHandleMaxSize+1, resource.ResourceHandleMaxSize)},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DriverName = "valid"
|
||||||
|
claim.Status.Allocation = &resource.AllocationResult{
|
||||||
|
ResourceHandle: strings.Repeat(" ", resource.ResourceHandleMaxSize+1),
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-node-selector": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("status", "allocation", "availableOnNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DriverName = "valid"
|
||||||
|
claim.Status.Allocation = &resource.AllocationResult{
|
||||||
|
AvailableOnNodes: &core.NodeSelector{
|
||||||
|
// Must not be empty.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"add-reservation": {
|
||||||
|
oldClaim: validAllocatedClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
for i := 0; i < resource.ResourceClaimReservedForMaxSize; i++ {
|
||||||
|
claim.Status.ReservedFor = append(claim.Status.ReservedFor,
|
||||||
|
resource.ResourceClaimConsumerReference{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: fmt.Sprintf("foo-%d", i),
|
||||||
|
UID: "1",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"add-reservation-and-allocation": {
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status = *validAllocatedClaim.Status.DeepCopy()
|
||||||
|
for i := 0; i < resource.ResourceClaimReservedForMaxSize; i++ {
|
||||||
|
claim.Status.ReservedFor = append(claim.Status.ReservedFor,
|
||||||
|
resource.ResourceClaimConsumerReference{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: fmt.Sprintf("foo-%d", i),
|
||||||
|
UID: "1",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-for-too-large": {
|
||||||
|
wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "reservedFor"), resource.ResourceClaimReservedForMaxSize+1, resource.ResourceClaimReservedForMaxSize)},
|
||||||
|
oldClaim: validAllocatedClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
for i := 0; i < resource.ResourceClaimReservedForMaxSize+1; i++ {
|
||||||
|
claim.Status.ReservedFor = append(claim.Status.ReservedFor,
|
||||||
|
resource.ResourceClaimConsumerReference{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: fmt.Sprintf("foo-%d", i),
|
||||||
|
UID: "1",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-for-duplicate": {
|
||||||
|
wantFailures: field.ErrorList{field.Duplicate(field.NewPath("status", "reservedFor").Index(1), resource.ResourceClaimConsumerReference{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "1",
|
||||||
|
})},
|
||||||
|
oldClaim: validAllocatedClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
claim.Status.ReservedFor = append(claim.Status.ReservedFor,
|
||||||
|
resource.ResourceClaimConsumerReference{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "1",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-for-not-shared": {
|
||||||
|
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "may not be reserved more than once")},
|
||||||
|
oldClaim: func() *resource.ResourceClaim {
|
||||||
|
claim := validAllocatedClaim.DeepCopy()
|
||||||
|
claim.Status.Allocation.Shareable = false
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
claim.Status.ReservedFor = append(claim.Status.ReservedFor,
|
||||||
|
resource.ResourceClaimConsumerReference{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: fmt.Sprintf("foo-%d", i),
|
||||||
|
UID: "1",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-for-no-allocation": {
|
||||||
|
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "may not be specified when `allocated` is not set")},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DriverName = "valid"
|
||||||
|
claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{
|
||||||
|
{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-for-no-resource": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("resource"), "")},
|
||||||
|
oldClaim: validAllocatedClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
UID: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-for-no-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("name"), "")},
|
||||||
|
oldClaim: validAllocatedClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{
|
||||||
|
{
|
||||||
|
Resource: "pods",
|
||||||
|
UID: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-for-no-uid": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("uid"), "")},
|
||||||
|
oldClaim: validAllocatedClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{
|
||||||
|
{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-deleted": {
|
||||||
|
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set")},
|
||||||
|
oldClaim: func() *resource.ResourceClaim {
|
||||||
|
claim := validAllocatedClaim.DeepCopy()
|
||||||
|
var deletionTimestamp metav1.Time
|
||||||
|
claim.DeletionTimestamp = &deletionTimestamp
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{
|
||||||
|
{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-reserved-deallocation-requested": {
|
||||||
|
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set")},
|
||||||
|
oldClaim: func() *resource.ResourceClaim {
|
||||||
|
claim := validAllocatedClaim.DeepCopy()
|
||||||
|
claim.Status.DeallocationRequested = true
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{
|
||||||
|
{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"add-deallocation-requested": {
|
||||||
|
oldClaim: validAllocatedClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DeallocationRequested = true
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-deallocation-requested-removal": {
|
||||||
|
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "deallocationRequested"), "may not be cleared when `allocation` is set")},
|
||||||
|
oldClaim: func() *resource.ResourceClaim {
|
||||||
|
claim := validAllocatedClaim.DeepCopy()
|
||||||
|
claim.Status.DeallocationRequested = true
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DeallocationRequested = false
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-deallocation-requested-in-use": {
|
||||||
|
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "deallocationRequested"), "deallocation cannot be requested while `reservedFor` is set")},
|
||||||
|
oldClaim: func() *resource.ResourceClaim {
|
||||||
|
claim := validAllocatedClaim.DeepCopy()
|
||||||
|
claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{
|
||||||
|
{
|
||||||
|
Resource: "pods",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DeallocationRequested = true
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-deallocation-not-allocated": {
|
||||||
|
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status"), "`allocation` must be set when `deallocationRequested` is set")},
|
||||||
|
oldClaim: validClaim,
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.DeallocationRequested = true
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-allocation-removal-not-reset": {
|
||||||
|
wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status"), "`allocation` must be set when `deallocationRequested` is set")},
|
||||||
|
oldClaim: func() *resource.ResourceClaim {
|
||||||
|
claim := validAllocatedClaim.DeepCopy()
|
||||||
|
claim.Status.DeallocationRequested = true
|
||||||
|
return claim
|
||||||
|
}(),
|
||||||
|
update: func(claim *resource.ResourceClaim) *resource.ResourceClaim {
|
||||||
|
claim.Status.Allocation = nil
|
||||||
|
return claim
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario.oldClaim.ResourceVersion = "1"
|
||||||
|
errs := ValidateClaimStatusUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,313 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testClaimTemplate(name, namespace string, spec resource.ResourceClaimSpec) *resource.ResourceClaimTemplate {
|
||||||
|
return &resource.ResourceClaimTemplate{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimTemplateSpec{
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateClaimTemplate(t *testing.T) {
|
||||||
|
validMode := resource.AllocationModeImmediate
|
||||||
|
invalidMode := resource.AllocationMode("invalid")
|
||||||
|
goodName := "foo"
|
||||||
|
badName := "!@#$%^"
|
||||||
|
goodNS := "ns"
|
||||||
|
goodClaimSpec := resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: goodName,
|
||||||
|
AllocationMode: validMode,
|
||||||
|
}
|
||||||
|
now := metav1.Now()
|
||||||
|
badValue := "spaces not allowed"
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
template *resource.ResourceClaimTemplate
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"good-claim": {
|
||||||
|
template: testClaimTemplate(goodName, goodNS, goodClaimSpec),
|
||||||
|
},
|
||||||
|
"missing-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||||
|
template: testClaimTemplate("", goodNS, goodClaimSpec),
|
||||||
|
},
|
||||||
|
"bad-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
|
template: testClaimTemplate(badName, goodNS, goodClaimSpec),
|
||||||
|
},
|
||||||
|
"missing-namespace": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
|
||||||
|
template: testClaimTemplate(goodName, "", goodClaimSpec),
|
||||||
|
},
|
||||||
|
"generate-name": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.GenerateName = "pvc-"
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"resource-version": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.ResourceVersion = "1"
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Generation = 100
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"creation-timestamp": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.CreationTimestamp = now
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"deletion-grace-period-seconds": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.DeletionGracePeriodSeconds = pointer.Int64(10)
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"owner-references": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.OwnerReferences = []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: "v1",
|
||||||
|
Kind: "pod",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"finalizers": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Finalizers = []string{
|
||||||
|
"example.com/foo",
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"managed-fields": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||||
|
{
|
||||||
|
FieldsType: "FieldsV1",
|
||||||
|
Operation: "Apply",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
Manager: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-labels": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Labels = map[string]string{
|
||||||
|
"apps.kubernetes.io/name": "test",
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-labels": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Labels = map[string]string{
|
||||||
|
"hello-world": badValue,
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-annotations": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Annotations = map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-annotations": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Annotations = map[string]string{
|
||||||
|
badName: "hello world",
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-classname": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "spec", "resourceClassName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Spec.Spec.ResourceClassName = badName
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-mode": {
|
||||||
|
wantFailures: field.ErrorList{field.NotSupported(field.NewPath("spec", "spec", "allocationMode"), invalidMode, supportedAllocationModes.List())},
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Spec.Spec.AllocationMode = invalidMode
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-parameters": {
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
|
||||||
|
Kind: "foo",
|
||||||
|
Name: "bar",
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"missing-parameters-kind": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "spec", "parametersRef", "kind"), "")},
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
|
||||||
|
Name: "bar",
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"missing-parameters-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "spec", "parametersRef", "name"), "")},
|
||||||
|
template: func() *resource.ResourceClaimTemplate {
|
||||||
|
template := testClaimTemplate(goodName, goodNS, goodClaimSpec)
|
||||||
|
template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{
|
||||||
|
Kind: "foo",
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
errs := ValidateClaimTemplate(scenario.template)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateClaimTemplateUpdate(t *testing.T) {
|
||||||
|
name := "valid"
|
||||||
|
parameters := &resource.ResourceClaimParametersReference{
|
||||||
|
Kind: "foo",
|
||||||
|
Name: "bar",
|
||||||
|
}
|
||||||
|
validClaimTemplate := testClaimTemplate("foo", "ns", resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: name,
|
||||||
|
AllocationMode: resource.AllocationModeImmediate,
|
||||||
|
ParametersRef: parameters,
|
||||||
|
})
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
oldClaimTemplate *resource.ResourceClaimTemplate
|
||||||
|
update func(claim *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"valid-no-op-update": {
|
||||||
|
oldClaimTemplate: validClaimTemplate,
|
||||||
|
update: func(claim *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate { return claim },
|
||||||
|
},
|
||||||
|
"invalid-update-class": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
|
||||||
|
spec := validClaimTemplate.Spec.DeepCopy()
|
||||||
|
spec.Spec.ResourceClassName += "2"
|
||||||
|
return *spec
|
||||||
|
}(), "field is immutable")},
|
||||||
|
oldClaimTemplate: validClaimTemplate,
|
||||||
|
update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
|
||||||
|
template.Spec.Spec.ResourceClassName += "2"
|
||||||
|
return template
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-update-remove-parameters": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
|
||||||
|
spec := validClaimTemplate.Spec.DeepCopy()
|
||||||
|
spec.Spec.ParametersRef = nil
|
||||||
|
return *spec
|
||||||
|
}(), "field is immutable")},
|
||||||
|
oldClaimTemplate: validClaimTemplate,
|
||||||
|
update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
|
||||||
|
template.Spec.Spec.ParametersRef = nil
|
||||||
|
return template
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-update-mode": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec {
|
||||||
|
spec := validClaimTemplate.Spec.DeepCopy()
|
||||||
|
spec.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
|
||||||
|
return *spec
|
||||||
|
}(), "field is immutable")},
|
||||||
|
oldClaimTemplate: validClaimTemplate,
|
||||||
|
update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate {
|
||||||
|
template.Spec.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer
|
||||||
|
return template
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario.oldClaimTemplate.ResourceVersion = "1"
|
||||||
|
errs := ValidateClaimTemplateUpdate(scenario.update(scenario.oldClaimTemplate.DeepCopy()), scenario.oldClaimTemplate)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
282
pkg/apis/resource/validation/validation_resourceclass_test.go
Normal file
282
pkg/apis/resource/validation/validation_resourceclass_test.go
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/core"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testClass(name, driverName string) *resource.ResourceClass {
|
||||||
|
return &resource.ResourceClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
DriverName: driverName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateClass(t *testing.T) {
|
||||||
|
goodName := "foo"
|
||||||
|
now := metav1.Now()
|
||||||
|
goodParameters := resource.ResourceClassParametersReference{
|
||||||
|
Name: "valid",
|
||||||
|
Namespace: "valid",
|
||||||
|
Kind: "foo",
|
||||||
|
}
|
||||||
|
badName := "!@#$%^"
|
||||||
|
badValue := "spaces not allowed"
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
class *resource.ResourceClass
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"good-class": {
|
||||||
|
class: testClass(goodName, goodName),
|
||||||
|
},
|
||||||
|
"good-long-driver-name": {
|
||||||
|
class: testClass(goodName, "acme.example.com"),
|
||||||
|
},
|
||||||
|
"missing-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
|
||||||
|
class: testClass("", goodName),
|
||||||
|
},
|
||||||
|
"bad-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
|
class: testClass(badName, goodName),
|
||||||
|
},
|
||||||
|
"generate-name": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.GenerateName = "pvc-"
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"resource-version": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.ResourceVersion = "1"
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"generation": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.Generation = 100
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"creation-timestamp": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.CreationTimestamp = now
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"deletion-grace-period-seconds": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.DeletionGracePeriodSeconds = pointer.Int64(10)
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"owner-references": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.OwnerReferences = []metav1.OwnerReference{
|
||||||
|
{
|
||||||
|
APIVersion: "v1",
|
||||||
|
Kind: "pod",
|
||||||
|
Name: "foo",
|
||||||
|
UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"finalizers": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.Finalizers = []string{
|
||||||
|
"example.com/foo",
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"managed-fields": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.ManagedFields = []metav1.ManagedFieldsEntry{
|
||||||
|
{
|
||||||
|
FieldsType: "FieldsV1",
|
||||||
|
Operation: "Apply",
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
Manager: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-labels": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.Labels = map[string]string{
|
||||||
|
"apps.kubernetes.io/name": "test",
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-labels": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.Labels = map[string]string{
|
||||||
|
"hello-world": badValue,
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"good-annotations": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.Annotations = map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-annotations": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.Annotations = map[string]string{
|
||||||
|
badName: "hello world",
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"missing-driver-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("driverName"), ""),
|
||||||
|
field.Invalid(field.NewPath("driverName"), "", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')"),
|
||||||
|
},
|
||||||
|
class: testClass(goodName, ""),
|
||||||
|
},
|
||||||
|
"invalid-driver-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
|
class: testClass(goodName, badName),
|
||||||
|
},
|
||||||
|
"invalid-qualified-driver-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), goodName+"/path", "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
|
||||||
|
class: testClass(goodName, goodName+"/path"),
|
||||||
|
},
|
||||||
|
"good-parameters": {
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.ParametersRef = goodParameters.DeepCopy()
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"missing-parameters-name": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "name"), "")},
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.ParametersRef = goodParameters.DeepCopy()
|
||||||
|
class.ParametersRef.Name = ""
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"bad-parameters-namespace": {
|
||||||
|
wantFailures: field.ErrorList{field.Invalid(field.NewPath("parametersRef", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.ParametersRef = goodParameters.DeepCopy()
|
||||||
|
class.ParametersRef.Namespace = badName
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"missing-parameters-kind": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "kind"), "")},
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.ParametersRef = goodParameters.DeepCopy()
|
||||||
|
class.ParametersRef.Kind = ""
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
"invalid-node-selector": {
|
||||||
|
wantFailures: field.ErrorList{field.Required(field.NewPath("suitableNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
|
||||||
|
class: func() *resource.ResourceClass {
|
||||||
|
class := testClass(goodName, goodName)
|
||||||
|
class.SuitableNodes = &core.NodeSelector{
|
||||||
|
// Must not be empty.
|
||||||
|
}
|
||||||
|
return class
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
errs := ValidateClass(scenario.class)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateClassUpdate(t *testing.T) {
|
||||||
|
validClass := testClass("foo", "valid")
|
||||||
|
|
||||||
|
scenarios := map[string]struct {
|
||||||
|
oldClass *resource.ResourceClass
|
||||||
|
update func(class *resource.ResourceClass) *resource.ResourceClass
|
||||||
|
wantFailures field.ErrorList
|
||||||
|
}{
|
||||||
|
"valid-no-op-update": {
|
||||||
|
oldClass: validClass,
|
||||||
|
update: func(class *resource.ResourceClass) *resource.ResourceClass { return class },
|
||||||
|
},
|
||||||
|
"update-driver": {
|
||||||
|
oldClass: validClass,
|
||||||
|
update: func(class *resource.ResourceClass) *resource.ResourceClass {
|
||||||
|
class.DriverName += "2"
|
||||||
|
return class
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, scenario := range scenarios {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
scenario.oldClass.ResourceVersion = "1"
|
||||||
|
errs := ValidateClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
|
||||||
|
assert.Equal(t, scenario.wantFailures, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ import (
|
|||||||
_ "k8s.io/kubernetes/pkg/apis/node/install"
|
_ "k8s.io/kubernetes/pkg/apis/node/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/policy/install"
|
_ "k8s.io/kubernetes/pkg/apis/policy/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
|
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/storage/install"
|
_ "k8s.io/kubernetes/pkg/apis/storage/install"
|
||||||
)
|
)
|
||||||
|
@ -54,6 +54,7 @@ import (
|
|||||||
policyapiv1 "k8s.io/api/policy/v1"
|
policyapiv1 "k8s.io/api/policy/v1"
|
||||||
policyapiv1beta1 "k8s.io/api/policy/v1beta1"
|
policyapiv1beta1 "k8s.io/api/policy/v1beta1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
resourcev1alpha1 "k8s.io/api/resource/v1alpha1"
|
||||||
schedulingapiv1 "k8s.io/api/scheduling/v1"
|
schedulingapiv1 "k8s.io/api/scheduling/v1"
|
||||||
storageapiv1 "k8s.io/api/storage/v1"
|
storageapiv1 "k8s.io/api/storage/v1"
|
||||||
storageapiv1alpha1 "k8s.io/api/storage/v1alpha1"
|
storageapiv1alpha1 "k8s.io/api/storage/v1alpha1"
|
||||||
@ -108,6 +109,7 @@ import (
|
|||||||
noderest "k8s.io/kubernetes/pkg/registry/node/rest"
|
noderest "k8s.io/kubernetes/pkg/registry/node/rest"
|
||||||
policyrest "k8s.io/kubernetes/pkg/registry/policy/rest"
|
policyrest "k8s.io/kubernetes/pkg/registry/policy/rest"
|
||||||
rbacrest "k8s.io/kubernetes/pkg/registry/rbac/rest"
|
rbacrest "k8s.io/kubernetes/pkg/registry/rbac/rest"
|
||||||
|
resourcerest "k8s.io/kubernetes/pkg/registry/resource/rest"
|
||||||
schedulingrest "k8s.io/kubernetes/pkg/registry/scheduling/rest"
|
schedulingrest "k8s.io/kubernetes/pkg/registry/scheduling/rest"
|
||||||
storagerest "k8s.io/kubernetes/pkg/registry/storage/rest"
|
storagerest "k8s.io/kubernetes/pkg/registry/storage/rest"
|
||||||
)
|
)
|
||||||
@ -435,6 +437,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
|||||||
appsrest.StorageProvider{},
|
appsrest.StorageProvider{},
|
||||||
admissionregistrationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer, DiscoveryClient: discoveryClientForAdmissionRegistration},
|
admissionregistrationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer, DiscoveryClient: discoveryClientForAdmissionRegistration},
|
||||||
eventsrest.RESTStorageProvider{TTL: c.ExtraConfig.EventTTL},
|
eventsrest.RESTStorageProvider{TTL: c.ExtraConfig.EventTTL},
|
||||||
|
resourcerest.RESTStorageProvider{},
|
||||||
}
|
}
|
||||||
if err := m.InstallAPIs(c.ExtraConfig.APIResourceConfigSource, c.GenericConfig.RESTOptionsGetter, restStorageProviders...); err != nil {
|
if err := m.InstallAPIs(c.ExtraConfig.APIResourceConfigSource, c.GenericConfig.RESTOptionsGetter, restStorageProviders...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -704,6 +707,7 @@ var (
|
|||||||
admissionregistrationv1alpha1.SchemeGroupVersion,
|
admissionregistrationv1alpha1.SchemeGroupVersion,
|
||||||
apiserverinternalv1alpha1.SchemeGroupVersion,
|
apiserverinternalv1alpha1.SchemeGroupVersion,
|
||||||
authenticationv1alpha1.SchemeGroupVersion,
|
authenticationv1alpha1.SchemeGroupVersion,
|
||||||
|
resourcev1alpha1.SchemeGroupVersion,
|
||||||
networkingapiv1alpha1.SchemeGroupVersion,
|
networkingapiv1alpha1.SchemeGroupVersion,
|
||||||
storageapiv1alpha1.SchemeGroupVersion,
|
storageapiv1alpha1.SchemeGroupVersion,
|
||||||
flowcontrolv1alpha1.SchemeGroupVersion,
|
flowcontrolv1alpha1.SchemeGroupVersion,
|
||||||
|
@ -78,6 +78,9 @@ rules:
|
|||||||
- k8s.io/kubernetes/pkg/apis/rbac/v1
|
- k8s.io/kubernetes/pkg/apis/rbac/v1
|
||||||
- k8s.io/kubernetes/pkg/apis/rbac/v1alpha1
|
- k8s.io/kubernetes/pkg/apis/rbac/v1alpha1
|
||||||
- k8s.io/kubernetes/pkg/apis/rbac/v1beta1
|
- k8s.io/kubernetes/pkg/apis/rbac/v1beta1
|
||||||
|
- k8s.io/kubernetes/pkg/apis/resource
|
||||||
|
- k8s.io/kubernetes/pkg/apis/resource/install
|
||||||
|
- k8s.io/kubernetes/pkg/apis/resource/v1alpha1
|
||||||
- k8s.io/kubernetes/pkg/apis/scheduling
|
- k8s.io/kubernetes/pkg/apis/scheduling
|
||||||
- k8s.io/kubernetes/pkg/apis/scheduling/install
|
- k8s.io/kubernetes/pkg/apis/scheduling/install
|
||||||
- k8s.io/kubernetes/pkg/apis/scheduling/v1alpha1
|
- k8s.io/kubernetes/pkg/apis/scheduling/v1alpha1
|
||||||
|
@ -38,6 +38,7 @@ import (
|
|||||||
_ "k8s.io/kubernetes/pkg/apis/node/install"
|
_ "k8s.io/kubernetes/pkg/apis/node/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/policy/install"
|
_ "k8s.io/kubernetes/pkg/apis/policy/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
|
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/storage/install"
|
_ "k8s.io/kubernetes/pkg/apis/storage/install"
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
_ "k8s.io/kubernetes/pkg/apis/extensions/install"
|
_ "k8s.io/kubernetes/pkg/apis/extensions/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/policy/install"
|
_ "k8s.io/kubernetes/pkg/apis/policy/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
|
_ "k8s.io/kubernetes/pkg/apis/scheduling/install"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/storage/install"
|
_ "k8s.io/kubernetes/pkg/apis/storage/install"
|
||||||
)
|
)
|
||||||
|
6
pkg/registry/resource/OWNERS
Normal file
6
pkg/registry/resource/OWNERS
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# See the OWNERS docs at https://go.k8s.io/owners
|
||||||
|
|
||||||
|
reviewers:
|
||||||
|
- bart0sh
|
||||||
|
- klueska
|
||||||
|
- pohly
|
100
pkg/registry/resource/podscheduling/storage/storage.go
Normal file
100
pkg/registry/resource/podscheduling/storage/storage.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/printers"
|
||||||
|
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||||
|
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/resource/podscheduling"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// REST implements a RESTStorage for PodSchedulings.
|
||||||
|
type REST struct {
|
||||||
|
*genericregistry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewREST returns a RESTStorage object that will work against PodSchedulings.
|
||||||
|
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, error) {
|
||||||
|
store := &genericregistry.Store{
|
||||||
|
NewFunc: func() runtime.Object { return &resource.PodScheduling{} },
|
||||||
|
NewListFunc: func() runtime.Object { return &resource.PodSchedulingList{} },
|
||||||
|
PredicateFunc: podscheduling.Match,
|
||||||
|
DefaultQualifiedResource: resource.Resource("podschedulings"),
|
||||||
|
|
||||||
|
CreateStrategy: podscheduling.Strategy,
|
||||||
|
UpdateStrategy: podscheduling.Strategy,
|
||||||
|
DeleteStrategy: podscheduling.Strategy,
|
||||||
|
ReturnDeletedObject: true,
|
||||||
|
ResetFieldsStrategy: podscheduling.Strategy,
|
||||||
|
|
||||||
|
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||||
|
}
|
||||||
|
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: podscheduling.GetAttrs}
|
||||||
|
if err := store.CompleteWithOptions(options); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statusStore := *store
|
||||||
|
statusStore.UpdateStrategy = podscheduling.StatusStrategy
|
||||||
|
statusStore.ResetFieldsStrategy = podscheduling.StatusStrategy
|
||||||
|
|
||||||
|
rest := &REST{store}
|
||||||
|
|
||||||
|
return rest, &StatusREST{store: &statusStore}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusREST implements the REST endpoint for changing the status of a PodScheduling.
|
||||||
|
type StatusREST struct {
|
||||||
|
store *genericregistry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new PodScheduling object.
|
||||||
|
func (r *StatusREST) New() runtime.Object {
|
||||||
|
return &resource.PodScheduling{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusREST) Destroy() {
|
||||||
|
// Given that underlying store is shared with REST,
|
||||||
|
// we don't destroy it here explicitly.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves the object from the storage. It is required to support Patch.
|
||||||
|
func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||||
|
return r.store.Get(ctx, name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update alters the status subset of an object.
|
||||||
|
func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||||
|
// We are explicitly setting forceAllowCreate to false in the call to the underlying storage because
|
||||||
|
// subresources should never allow create on update.
|
||||||
|
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResetFields implements rest.ResetFieldsStrategy
|
||||||
|
func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||||
|
return r.store.GetResetFields()
|
||||||
|
}
|
184
pkg/registry/resource/podscheduling/storage/storage_test.go
Normal file
184
pkg/registry/resource/podscheduling/storage/storage_test.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStorage(t *testing.T) (*REST, *StatusREST, *etcd3testing.EtcdTestServer) {
|
||||||
|
etcdStorage, server := registrytest.NewEtcdStorage(t, resource.GroupName)
|
||||||
|
restOptions := generic.RESTOptions{
|
||||||
|
StorageConfig: etcdStorage,
|
||||||
|
Decorator: generic.UndecoratedStorage,
|
||||||
|
DeleteCollectionWorkers: 1,
|
||||||
|
ResourcePrefix: "podschedulings",
|
||||||
|
}
|
||||||
|
podSchedulingStorage, statusStorage, err := NewREST(restOptions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
|
}
|
||||||
|
return podSchedulingStorage, statusStorage, server
|
||||||
|
}
|
||||||
|
|
||||||
|
func validNewPodScheduling(name, ns string) *resource.PodScheduling {
|
||||||
|
scheduling := &resource.PodScheduling{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: ns,
|
||||||
|
},
|
||||||
|
Spec: resource.PodSchedulingSpec{
|
||||||
|
SelectedNode: "worker",
|
||||||
|
},
|
||||||
|
Status: resource.PodSchedulingStatus{},
|
||||||
|
}
|
||||||
|
return scheduling
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
scheduling := validNewPodScheduling("foo", metav1.NamespaceDefault)
|
||||||
|
scheduling.ObjectMeta = metav1.ObjectMeta{}
|
||||||
|
test.TestCreate(
|
||||||
|
// valid
|
||||||
|
scheduling,
|
||||||
|
// invalid
|
||||||
|
&resource.PodScheduling{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestUpdate(
|
||||||
|
// valid
|
||||||
|
validNewPodScheduling("foo", metav1.NamespaceDefault),
|
||||||
|
// updateFunc
|
||||||
|
func(obj runtime.Object) runtime.Object {
|
||||||
|
object := obj.(*resource.PodScheduling)
|
||||||
|
if object.Labels == nil {
|
||||||
|
object.Labels = map[string]string{}
|
||||||
|
}
|
||||||
|
object.Labels["foo"] = "bar"
|
||||||
|
return object
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ReturnDeletedObject()
|
||||||
|
test.TestDelete(validNewPodScheduling("foo", metav1.NamespaceDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestGet(validNewPodScheduling("foo", metav1.NamespaceDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestList(validNewPodScheduling("foo", metav1.NamespaceDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestWatch(
|
||||||
|
validNewPodScheduling("foo", metav1.NamespaceDefault),
|
||||||
|
// matching labels
|
||||||
|
[]labels.Set{},
|
||||||
|
// not matching labels
|
||||||
|
[]labels.Set{
|
||||||
|
{"foo": "bar"},
|
||||||
|
},
|
||||||
|
// matching fields
|
||||||
|
[]fields.Set{
|
||||||
|
{"metadata.name": "foo"},
|
||||||
|
},
|
||||||
|
// not matching fields
|
||||||
|
[]fields.Set{
|
||||||
|
{"metadata.name": "bar"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStatus(t *testing.T) {
|
||||||
|
storage, statusStorage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
|
||||||
|
key, _ := storage.KeyFunc(ctx, "foo")
|
||||||
|
schedulingStart := validNewPodScheduling("foo", metav1.NamespaceDefault)
|
||||||
|
err := storage.Storage.Create(ctx, key, schedulingStart, nil, 0, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduling := schedulingStart.DeepCopy()
|
||||||
|
scheduling.Status.ResourceClaims = append(scheduling.Status.ResourceClaims,
|
||||||
|
resource.ResourceClaimSchedulingStatus{
|
||||||
|
Name: "my-claim",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_, _, err = statusStorage.Update(ctx, scheduling.Name, rest.DefaultUpdatedObjectInfo(scheduling), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
obj, err := storage.Get(ctx, "foo", &metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
schedulingOut := obj.(*resource.PodScheduling)
|
||||||
|
// only compare relevant changes b/c of difference in metadata
|
||||||
|
if !apiequality.Semantic.DeepEqual(scheduling.Status, schedulingOut.Status) {
|
||||||
|
t.Errorf("unexpected object: %s", diff.ObjectDiff(scheduling.Status, schedulingOut.Status))
|
||||||
|
}
|
||||||
|
}
|
163
pkg/registry/resource/podscheduling/strategy.go
Normal file
163
pkg/registry/resource/podscheduling/strategy.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 podscheduling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
"k8s.io/apiserver/pkg/storage"
|
||||||
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource/validation"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// podSchedulingStrategy implements behavior for PodScheduling objects
|
||||||
|
type podSchedulingStrategy struct {
|
||||||
|
runtime.ObjectTyper
|
||||||
|
names.NameGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy is the default logic that applies when creating and updating
|
||||||
|
// ResourceClaim objects via the REST API.
|
||||||
|
var Strategy = podSchedulingStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) NamespaceScoped() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResetFields returns the set of fields that get reset by the strategy and
|
||||||
|
// should not be modified by the user. For a new PodScheduling that is the
|
||||||
|
// status.
|
||||||
|
func (podSchedulingStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||||
|
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
||||||
|
"resource.k8s.io/v1alpha1": fieldpath.NewSet(
|
||||||
|
fieldpath.MakePathOrDie("status"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||||
|
scheduling := obj.(*resource.PodScheduling)
|
||||||
|
// Status must not be set by user on create.
|
||||||
|
scheduling.Status = resource.PodSchedulingStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||||
|
scheduling := obj.(*resource.PodScheduling)
|
||||||
|
return validation.ValidatePodScheduling(scheduling)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) Canonicalize(obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) AllowCreateOnUpdate() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
|
newScheduling := obj.(*resource.PodScheduling)
|
||||||
|
oldScheduling := old.(*resource.PodScheduling)
|
||||||
|
newScheduling.Status = oldScheduling.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
newScheduling := obj.(*resource.PodScheduling)
|
||||||
|
oldScheduling := old.(*resource.PodScheduling)
|
||||||
|
errorList := validation.ValidatePodScheduling(newScheduling)
|
||||||
|
return append(errorList, validation.ValidatePodSchedulingUpdate(newScheduling, oldScheduling)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStrategy) AllowUnconditionalUpdate() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type podSchedulingStatusStrategy struct {
|
||||||
|
podSchedulingStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
var StatusStrategy = podSchedulingStatusStrategy{Strategy}
|
||||||
|
|
||||||
|
// GetResetFields returns the set of fields that get reset by the strategy and
|
||||||
|
// should not be modified by the user. For a status update that is the spec.
|
||||||
|
func (podSchedulingStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||||
|
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
||||||
|
"resource.k8s.io/v1alpha1": fieldpath.NewSet(
|
||||||
|
fieldpath.MakePathOrDie("spec"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
|
newScheduling := obj.(*resource.PodScheduling)
|
||||||
|
oldScheduling := old.(*resource.PodScheduling)
|
||||||
|
newScheduling.Spec = oldScheduling.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (podSchedulingStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
newScheduling := obj.(*resource.PodScheduling)
|
||||||
|
oldScheduling := old.(*resource.PodScheduling)
|
||||||
|
return validation.ValidatePodSchedulingStatusUpdate(newScheduling, oldScheduling)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarningsOnUpdate returns warnings for the given update.
|
||||||
|
func (podSchedulingStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns a generic matcher for a given label and field selector.
|
||||||
|
func Match(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
|
||||||
|
return storage.SelectionPredicate{
|
||||||
|
Label: label,
|
||||||
|
Field: field,
|
||||||
|
GetAttrs: GetAttrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAttrs returns labels and fields of a given object for filtering purposes.
|
||||||
|
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
||||||
|
scheduling, ok := obj.(*resource.PodScheduling)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("not a PodScheduling")
|
||||||
|
}
|
||||||
|
return labels.Set(scheduling.Labels), toSelectableFields(scheduling), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toSelectableFields returns a field set that represents the object
|
||||||
|
func toSelectableFields(scheduling *resource.PodScheduling) fields.Set {
|
||||||
|
fields := generic.ObjectMetaFieldsSet(&scheduling.ObjectMeta, true)
|
||||||
|
return fields
|
||||||
|
}
|
84
pkg/registry/resource/podscheduling/strategy_test.go
Normal file
84
pkg/registry/resource/podscheduling/strategy_test.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 podscheduling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
var podScheduling = &resource.PodScheduling{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-pod",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: resource.PodSchedulingSpec{
|
||||||
|
SelectedNode: "worker",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodSchedulingStrategy(t *testing.T) {
|
||||||
|
if !Strategy.NamespaceScoped() {
|
||||||
|
t.Errorf("PodScheduling must be namespace scoped")
|
||||||
|
}
|
||||||
|
if Strategy.AllowCreateOnUpdate() {
|
||||||
|
t.Errorf("PodScheduling should not allow create on update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodSchedulingStrategyCreate(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
podScheduling := podScheduling.DeepCopy()
|
||||||
|
|
||||||
|
Strategy.PrepareForCreate(ctx, podScheduling)
|
||||||
|
errs := Strategy.Validate(ctx, podScheduling)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("unexpected error validating for create %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPodSchedulingStrategyUpdate(t *testing.T) {
|
||||||
|
t.Run("no-changes-okay", func(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
podScheduling := podScheduling.DeepCopy()
|
||||||
|
newPodScheduling := podScheduling.DeepCopy()
|
||||||
|
newPodScheduling.ResourceVersion = "4"
|
||||||
|
|
||||||
|
Strategy.PrepareForUpdate(ctx, newPodScheduling, podScheduling)
|
||||||
|
errs := Strategy.ValidateUpdate(ctx, newPodScheduling, podScheduling)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("unexpected validation errors: %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("name-change-not-allowed", func(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
podScheduling := podScheduling.DeepCopy()
|
||||||
|
newPodScheduling := podScheduling.DeepCopy()
|
||||||
|
newPodScheduling.Name = "valid-claim-2"
|
||||||
|
newPodScheduling.ResourceVersion = "4"
|
||||||
|
|
||||||
|
Strategy.PrepareForUpdate(ctx, newPodScheduling, podScheduling)
|
||||||
|
errs := Strategy.ValidateUpdate(ctx, newPodScheduling, podScheduling)
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Errorf("expected a validation error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
100
pkg/registry/resource/resourceclaim/storage/storage.go
Normal file
100
pkg/registry/resource/resourceclaim/storage/storage.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/printers"
|
||||||
|
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||||
|
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/resource/resourceclaim"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// REST implements a RESTStorage for ResourceClaims.
|
||||||
|
type REST struct {
|
||||||
|
*genericregistry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewREST returns a RESTStorage object that will work against ResourceClaims.
|
||||||
|
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, error) {
|
||||||
|
store := &genericregistry.Store{
|
||||||
|
NewFunc: func() runtime.Object { return &resource.ResourceClaim{} },
|
||||||
|
NewListFunc: func() runtime.Object { return &resource.ResourceClaimList{} },
|
||||||
|
PredicateFunc: resourceclaim.Match,
|
||||||
|
DefaultQualifiedResource: resource.Resource("resourceclaims"),
|
||||||
|
|
||||||
|
CreateStrategy: resourceclaim.Strategy,
|
||||||
|
UpdateStrategy: resourceclaim.Strategy,
|
||||||
|
DeleteStrategy: resourceclaim.Strategy,
|
||||||
|
ReturnDeletedObject: true,
|
||||||
|
ResetFieldsStrategy: resourceclaim.Strategy,
|
||||||
|
|
||||||
|
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||||
|
}
|
||||||
|
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: resourceclaim.GetAttrs}
|
||||||
|
if err := store.CompleteWithOptions(options); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statusStore := *store
|
||||||
|
statusStore.UpdateStrategy = resourceclaim.StatusStrategy
|
||||||
|
statusStore.ResetFieldsStrategy = resourceclaim.StatusStrategy
|
||||||
|
|
||||||
|
rest := &REST{store}
|
||||||
|
|
||||||
|
return rest, &StatusREST{store: &statusStore}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusREST implements the REST endpoint for changing the status of a ResourceClaim.
|
||||||
|
type StatusREST struct {
|
||||||
|
store *genericregistry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new ResourceClaim object.
|
||||||
|
func (r *StatusREST) New() runtime.Object {
|
||||||
|
return &resource.ResourceClaim{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusREST) Destroy() {
|
||||||
|
// Given that underlying store is shared with REST,
|
||||||
|
// we don't destroy it here explicitly.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves the object from the storage. It is required to support Patch.
|
||||||
|
func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||||
|
return r.store.Get(ctx, name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update alters the status subset of an object.
|
||||||
|
func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||||
|
// We are explicitly setting forceAllowCreate to false in the call to the underlying storage because
|
||||||
|
// subresources should never allow create on update.
|
||||||
|
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResetFields implements rest.ResetFieldsStrategy
|
||||||
|
func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||||
|
return r.store.GetResetFields()
|
||||||
|
}
|
182
pkg/registry/resource/resourceclaim/storage/storage_test.go
Normal file
182
pkg/registry/resource/resourceclaim/storage/storage_test.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStorage(t *testing.T) (*REST, *StatusREST, *etcd3testing.EtcdTestServer) {
|
||||||
|
etcdStorage, server := registrytest.NewEtcdStorage(t, resource.GroupName)
|
||||||
|
restOptions := generic.RESTOptions{
|
||||||
|
StorageConfig: etcdStorage,
|
||||||
|
Decorator: generic.UndecoratedStorage,
|
||||||
|
DeleteCollectionWorkers: 1,
|
||||||
|
ResourcePrefix: "resourceclaims",
|
||||||
|
}
|
||||||
|
resourceClaimStorage, statusStorage, err := NewREST(restOptions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
|
}
|
||||||
|
return resourceClaimStorage, statusStorage, server
|
||||||
|
}
|
||||||
|
|
||||||
|
func validNewClaim(name, ns string) *resource.ResourceClaim {
|
||||||
|
claim := &resource.ResourceClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: ns,
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: "example",
|
||||||
|
AllocationMode: resource.AllocationModeImmediate,
|
||||||
|
},
|
||||||
|
Status: resource.ResourceClaimStatus{},
|
||||||
|
}
|
||||||
|
return claim
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
claim := validNewClaim("foo", metav1.NamespaceDefault)
|
||||||
|
claim.ObjectMeta = metav1.ObjectMeta{}
|
||||||
|
test.TestCreate(
|
||||||
|
// valid
|
||||||
|
claim,
|
||||||
|
// invalid
|
||||||
|
&resource.ResourceClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestUpdate(
|
||||||
|
// valid
|
||||||
|
validNewClaim("foo", metav1.NamespaceDefault),
|
||||||
|
// updateFunc
|
||||||
|
func(obj runtime.Object) runtime.Object {
|
||||||
|
object := obj.(*resource.ResourceClaim)
|
||||||
|
if object.Labels == nil {
|
||||||
|
object.Labels = map[string]string{}
|
||||||
|
}
|
||||||
|
object.Labels["foo"] = "bar"
|
||||||
|
return object
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ReturnDeletedObject()
|
||||||
|
test.TestDelete(validNewClaim("foo", metav1.NamespaceDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestGet(validNewClaim("foo", metav1.NamespaceDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestList(validNewClaim("foo", metav1.NamespaceDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
storage, _, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestWatch(
|
||||||
|
validNewClaim("foo", metav1.NamespaceDefault),
|
||||||
|
// matching labels
|
||||||
|
[]labels.Set{},
|
||||||
|
// not matching labels
|
||||||
|
[]labels.Set{
|
||||||
|
{"foo": "bar"},
|
||||||
|
},
|
||||||
|
// matching fields
|
||||||
|
[]fields.Set{
|
||||||
|
{"metadata.name": "foo"},
|
||||||
|
},
|
||||||
|
// not matching fields
|
||||||
|
[]fields.Set{
|
||||||
|
{"metadata.name": "bar"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateStatus(t *testing.T) {
|
||||||
|
storage, statusStorage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
|
||||||
|
key, _ := storage.KeyFunc(ctx, "foo")
|
||||||
|
claimStart := validNewClaim("foo", metav1.NamespaceDefault)
|
||||||
|
err := storage.Storage.Create(ctx, key, claimStart, nil, 0, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claim := claimStart.DeepCopy()
|
||||||
|
claim.Status.DriverName = "some-driver.example.com"
|
||||||
|
claim.Status.Allocation = &resource.AllocationResult{}
|
||||||
|
_, _, err = statusStorage.Update(ctx, claim.Name, rest.DefaultUpdatedObjectInfo(claim), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
obj, err := storage.Get(ctx, "foo", &metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
claimOut := obj.(*resource.ResourceClaim)
|
||||||
|
// only compare relevant changes b/c of difference in metadata
|
||||||
|
if !apiequality.Semantic.DeepEqual(claim.Status, claimOut.Status) {
|
||||||
|
t.Errorf("unexpected object: %s", diff.ObjectDiff(claim.Status, claimOut.Status))
|
||||||
|
}
|
||||||
|
}
|
163
pkg/registry/resource/resourceclaim/strategy.go
Normal file
163
pkg/registry/resource/resourceclaim/strategy.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 resourceclaim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
"k8s.io/apiserver/pkg/storage"
|
||||||
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource/validation"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resourceclaimStrategy implements behavior for ResourceClaim objects
|
||||||
|
type resourceclaimStrategy struct {
|
||||||
|
runtime.ObjectTyper
|
||||||
|
names.NameGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy is the default logic that applies when creating and updating
|
||||||
|
// ResourceClaim objects via the REST API.
|
||||||
|
var Strategy = resourceclaimStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) NamespaceScoped() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResetFields returns the set of fields that get reset by the strategy and
|
||||||
|
// should not be modified by the user. For a new ResourceClaim that is the
|
||||||
|
// status.
|
||||||
|
func (resourceclaimStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||||
|
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
||||||
|
"resource.k8s.io/v1alpha1": fieldpath.NewSet(
|
||||||
|
fieldpath.MakePathOrDie("status"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||||
|
claim := obj.(*resource.ResourceClaim)
|
||||||
|
// Status must not be set by user on create.
|
||||||
|
claim.Status = resource.ResourceClaimStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||||
|
claim := obj.(*resource.ResourceClaim)
|
||||||
|
return validation.ValidateClaim(claim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) Canonicalize(obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) AllowCreateOnUpdate() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
|
newClaim := obj.(*resource.ResourceClaim)
|
||||||
|
oldClaim := old.(*resource.ResourceClaim)
|
||||||
|
newClaim.Status = oldClaim.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
newClaim := obj.(*resource.ResourceClaim)
|
||||||
|
oldClaim := old.(*resource.ResourceClaim)
|
||||||
|
errorList := validation.ValidateClaim(newClaim)
|
||||||
|
return append(errorList, validation.ValidateClaimUpdate(newClaim, oldClaim)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStrategy) AllowUnconditionalUpdate() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceclaimStatusStrategy struct {
|
||||||
|
resourceclaimStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
var StatusStrategy = resourceclaimStatusStrategy{Strategy}
|
||||||
|
|
||||||
|
// GetResetFields returns the set of fields that get reset by the strategy and
|
||||||
|
// should not be modified by the user. For a status update that is the spec.
|
||||||
|
func (resourceclaimStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||||
|
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
||||||
|
"resource.k8s.io/v1alpha1": fieldpath.NewSet(
|
||||||
|
fieldpath.MakePathOrDie("spec"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
|
newClaim := obj.(*resource.ResourceClaim)
|
||||||
|
oldClaim := old.(*resource.ResourceClaim)
|
||||||
|
newClaim.Spec = oldClaim.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceclaimStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
newClaim := obj.(*resource.ResourceClaim)
|
||||||
|
oldClaim := old.(*resource.ResourceClaim)
|
||||||
|
return validation.ValidateClaimStatusUpdate(newClaim, oldClaim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarningsOnUpdate returns warnings for the given update.
|
||||||
|
func (resourceclaimStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns a generic matcher for a given label and field selector.
|
||||||
|
func Match(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
|
||||||
|
return storage.SelectionPredicate{
|
||||||
|
Label: label,
|
||||||
|
Field: field,
|
||||||
|
GetAttrs: GetAttrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAttrs returns labels and fields of a given object for filtering purposes.
|
||||||
|
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
||||||
|
claim, ok := obj.(*resource.ResourceClaim)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("not a resourceclaim")
|
||||||
|
}
|
||||||
|
return labels.Set(claim.Labels), toSelectableFields(claim), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toSelectableFields returns a field set that represents the object
|
||||||
|
func toSelectableFields(claim *resource.ResourceClaim) fields.Set {
|
||||||
|
fields := generic.ObjectMetaFieldsSet(&claim.ObjectMeta, true)
|
||||||
|
return fields
|
||||||
|
}
|
85
pkg/registry/resource/resourceclaim/strategy_test.go
Normal file
85
pkg/registry/resource/resourceclaim/strategy_test.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 resourceclaim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resourceClaim = &resource.ResourceClaim{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-claim",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: "valid-class",
|
||||||
|
AllocationMode: resource.AllocationModeImmediate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaimStrategy(t *testing.T) {
|
||||||
|
if !Strategy.NamespaceScoped() {
|
||||||
|
t.Errorf("ResourceClaim must be namespace scoped")
|
||||||
|
}
|
||||||
|
if Strategy.AllowCreateOnUpdate() {
|
||||||
|
t.Errorf("ResourceClaim should not allow create on update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaimStrategyCreate(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClaim := resourceClaim.DeepCopy()
|
||||||
|
|
||||||
|
Strategy.PrepareForCreate(ctx, resourceClaim)
|
||||||
|
errs := Strategy.Validate(ctx, resourceClaim)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("unexpected error validating for create %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaimStrategyUpdate(t *testing.T) {
|
||||||
|
t.Run("no-changes-okay", func(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClaim := resourceClaim.DeepCopy()
|
||||||
|
newClaim := resourceClaim.DeepCopy()
|
||||||
|
newClaim.ResourceVersion = "4"
|
||||||
|
|
||||||
|
Strategy.PrepareForUpdate(ctx, newClaim, resourceClaim)
|
||||||
|
errs := Strategy.ValidateUpdate(ctx, newClaim, resourceClaim)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("unexpected validation errors: %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("name-change-not-allowed", func(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClaim := resourceClaim.DeepCopy()
|
||||||
|
newClaim := resourceClaim.DeepCopy()
|
||||||
|
newClaim.Name = "valid-claim-2"
|
||||||
|
newClaim.ResourceVersion = "4"
|
||||||
|
|
||||||
|
Strategy.PrepareForUpdate(ctx, newClaim, resourceClaim)
|
||||||
|
errs := Strategy.ValidateUpdate(ctx, newClaim, resourceClaim)
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Errorf("expected a validation error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/printers"
|
||||||
|
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||||
|
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/resource/resourceclaimtemplate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// REST implements a RESTStorage for ResourceClaimTemplate.
|
||||||
|
type REST struct {
|
||||||
|
*genericregistry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewREST returns a RESTStorage object that will work against ResourceClass.
|
||||||
|
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
|
||||||
|
store := &genericregistry.Store{
|
||||||
|
NewFunc: func() runtime.Object { return &resource.ResourceClaimTemplate{} },
|
||||||
|
NewListFunc: func() runtime.Object { return &resource.ResourceClaimTemplateList{} },
|
||||||
|
DefaultQualifiedResource: resource.Resource("resourceclaimtemplates"),
|
||||||
|
|
||||||
|
CreateStrategy: resourceclaimtemplate.Strategy,
|
||||||
|
UpdateStrategy: resourceclaimtemplate.Strategy,
|
||||||
|
DeleteStrategy: resourceclaimtemplate.Strategy,
|
||||||
|
ReturnDeletedObject: true,
|
||||||
|
|
||||||
|
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||||
|
}
|
||||||
|
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: resourceclaimtemplate.GetAttrs}
|
||||||
|
if err := store.CompleteWithOptions(options); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &REST{store}, nil
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
|
etcdStorage, server := registrytest.NewEtcdStorage(t, resource.GroupName)
|
||||||
|
restOptions := generic.RESTOptions{
|
||||||
|
StorageConfig: etcdStorage,
|
||||||
|
Decorator: generic.UndecoratedStorage,
|
||||||
|
DeleteCollectionWorkers: 1,
|
||||||
|
ResourcePrefix: "resourceclaimtemplates",
|
||||||
|
}
|
||||||
|
resourceClaimTemplateStorage, err := NewREST(restOptions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
|
}
|
||||||
|
return resourceClaimTemplateStorage, server
|
||||||
|
}
|
||||||
|
|
||||||
|
func validNewClaimTemplate(name string) *resource.ResourceClaimTemplate {
|
||||||
|
return &resource.ResourceClaimTemplate{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: metav1.NamespaceDefault,
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimTemplateSpec{
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: "valid-class",
|
||||||
|
AllocationMode: resource.AllocationModeImmediate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
resourceClaimTemplate := validNewClaimTemplate("foo")
|
||||||
|
resourceClaimTemplate.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"}
|
||||||
|
test.TestCreate(
|
||||||
|
// valid
|
||||||
|
resourceClaimTemplate,
|
||||||
|
// invalid
|
||||||
|
&resource.ResourceClaimTemplate{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestUpdate(
|
||||||
|
// valid
|
||||||
|
validNewClaimTemplate("foo"),
|
||||||
|
// updateFunc
|
||||||
|
func(obj runtime.Object) runtime.Object {
|
||||||
|
object := obj.(*resource.ResourceClaimTemplate)
|
||||||
|
object.Labels = map[string]string{"a": "b"}
|
||||||
|
return object
|
||||||
|
},
|
||||||
|
//invalid update
|
||||||
|
func(obj runtime.Object) runtime.Object {
|
||||||
|
object := obj.(*resource.ResourceClaimTemplate)
|
||||||
|
object.Spec.Spec.ResourceClassName = ""
|
||||||
|
return object
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ReturnDeletedObject()
|
||||||
|
test.TestDelete(validNewClaimTemplate("foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestGet(validNewClaimTemplate("foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestList(validNewClaimTemplate("foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store)
|
||||||
|
test.TestWatch(
|
||||||
|
validNewClaimTemplate("foo"),
|
||||||
|
// matching labels
|
||||||
|
[]labels.Set{},
|
||||||
|
// not matching labels
|
||||||
|
[]labels.Set{
|
||||||
|
{"foo": "bar"},
|
||||||
|
},
|
||||||
|
// matching fields
|
||||||
|
[]fields.Set{
|
||||||
|
{"metadata.name": "foo"},
|
||||||
|
},
|
||||||
|
// not matching fields
|
||||||
|
[]fields.Set{
|
||||||
|
{"metadata.name": "bar"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
94
pkg/registry/resource/resourceclaimtemplate/strategy.go
Normal file
94
pkg/registry/resource/resourceclaimtemplate/strategy.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 resourceclaimtemplate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resourceClaimTemplateStrategy implements behavior for ResourceClaimTemplate objects
|
||||||
|
type resourceClaimTemplateStrategy struct {
|
||||||
|
runtime.ObjectTyper
|
||||||
|
names.NameGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
var Strategy = resourceClaimTemplateStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) NamespaceScoped() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||||
|
resourceClaimTemplate := obj.(*resource.ResourceClaimTemplate)
|
||||||
|
return validation.ValidateClaimTemplate(resourceClaimTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) Canonicalize(obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) AllowCreateOnUpdate() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
errorList := validation.ValidateClaimTemplate(obj.(*resource.ResourceClaimTemplate))
|
||||||
|
return append(errorList, validation.ValidateClaimTemplateUpdate(obj.(*resource.ResourceClaimTemplate), old.(*resource.ResourceClaimTemplate))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClaimTemplateStrategy) AllowUnconditionalUpdate() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAttrs returns labels and fields of a given object for filtering purposes.
|
||||||
|
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
|
||||||
|
template, ok := obj.(*resource.ResourceClaimTemplate)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("not a resourceclaimtemplate")
|
||||||
|
}
|
||||||
|
return labels.Set(template.Labels), toSelectableFields(template), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toSelectableFields returns a field set that represents the object
|
||||||
|
func toSelectableFields(template *resource.ResourceClaimTemplate) fields.Set {
|
||||||
|
fields := generic.ObjectMetaFieldsSet(&template.ObjectMeta, true)
|
||||||
|
return fields
|
||||||
|
}
|
87
pkg/registry/resource/resourceclaimtemplate/strategy_test.go
Normal file
87
pkg/registry/resource/resourceclaimtemplate/strategy_test.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 resourceclaimtemplate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resourceClaimTemplate = &resource.ResourceClaimTemplate{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-claim-template",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: resource.ResourceClaimTemplateSpec{
|
||||||
|
Spec: resource.ResourceClaimSpec{
|
||||||
|
ResourceClassName: "valid-class",
|
||||||
|
AllocationMode: resource.AllocationModeImmediate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaimTemplateStrategy(t *testing.T) {
|
||||||
|
if !Strategy.NamespaceScoped() {
|
||||||
|
t.Errorf("ResourceClaimTemplate must be namespace scoped")
|
||||||
|
}
|
||||||
|
if Strategy.AllowCreateOnUpdate() {
|
||||||
|
t.Errorf("ResourceClaimTemplate should not allow create on update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaimTemplateStrategyCreate(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClaimTemplate := resourceClaimTemplate.DeepCopy()
|
||||||
|
|
||||||
|
Strategy.PrepareForCreate(ctx, resourceClaimTemplate)
|
||||||
|
errs := Strategy.Validate(ctx, resourceClaimTemplate)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("unexpected error validating for create %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaimTemplateStrategyUpdate(t *testing.T) {
|
||||||
|
t.Run("no-changes-okay", func(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClaimTemplate := resourceClaimTemplate.DeepCopy()
|
||||||
|
newClaimTemplate := resourceClaimTemplate.DeepCopy()
|
||||||
|
newClaimTemplate.ResourceVersion = "4"
|
||||||
|
|
||||||
|
Strategy.PrepareForUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
|
errs := Strategy.ValidateUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("unexpected validation errors: %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("name-change-not-allowed", func(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClaimTemplate := resourceClaimTemplate.DeepCopy()
|
||||||
|
newClaimTemplate := resourceClaimTemplate.DeepCopy()
|
||||||
|
newClaimTemplate.Name = "valid-class-2"
|
||||||
|
newClaimTemplate.ResourceVersion = "4"
|
||||||
|
|
||||||
|
Strategy.PrepareForUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
|
errs := Strategy.ValidateUpdate(ctx, newClaimTemplate, resourceClaimTemplate)
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Errorf("expected a validation error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
55
pkg/registry/resource/resourceclass/storage/storage.go
Normal file
55
pkg/registry/resource/resourceclass/storage/storage.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/printers"
|
||||||
|
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||||
|
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/resource/resourceclass"
|
||||||
|
)
|
||||||
|
|
||||||
|
// REST implements a RESTStorage for ResourceClass.
|
||||||
|
type REST struct {
|
||||||
|
*genericregistry.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewREST returns a RESTStorage object that will work against ResourceClass.
|
||||||
|
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
|
||||||
|
store := &genericregistry.Store{
|
||||||
|
NewFunc: func() runtime.Object { return &resource.ResourceClass{} },
|
||||||
|
NewListFunc: func() runtime.Object { return &resource.ResourceClassList{} },
|
||||||
|
DefaultQualifiedResource: resource.Resource("resourceclasses"),
|
||||||
|
|
||||||
|
CreateStrategy: resourceclass.Strategy,
|
||||||
|
UpdateStrategy: resourceclass.Strategy,
|
||||||
|
DeleteStrategy: resourceclass.Strategy,
|
||||||
|
ReturnDeletedObject: true,
|
||||||
|
|
||||||
|
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||||
|
}
|
||||||
|
options := &generic.StoreOptions{RESTOptions: optsGetter}
|
||||||
|
if err := store.CompleteWithOptions(options); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &REST{store}, nil
|
||||||
|
}
|
145
pkg/registry/resource/resourceclass/storage/storage_test.go
Normal file
145
pkg/registry/resource/resourceclass/storage/storage_test.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
_ "k8s.io/kubernetes/pkg/apis/resource/install"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
|
etcdStorage, server := registrytest.NewEtcdStorage(t, resource.GroupName)
|
||||||
|
restOptions := generic.RESTOptions{
|
||||||
|
StorageConfig: etcdStorage,
|
||||||
|
Decorator: generic.UndecoratedStorage,
|
||||||
|
DeleteCollectionWorkers: 1,
|
||||||
|
ResourcePrefix: "resourceclasses",
|
||||||
|
}
|
||||||
|
resourceClassStorage, err := NewREST(restOptions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
|
}
|
||||||
|
return resourceClassStorage, server
|
||||||
|
}
|
||||||
|
|
||||||
|
func validNewClass(name string) *resource.ResourceClass {
|
||||||
|
return &resource.ResourceClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
DriverName: "cdi.example.com",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||||
|
resourceClass := validNewClass("foo")
|
||||||
|
resourceClass.ObjectMeta = metav1.ObjectMeta{GenerateName: "foo"}
|
||||||
|
test.TestCreate(
|
||||||
|
// valid
|
||||||
|
resourceClass,
|
||||||
|
// invalid
|
||||||
|
&resource.ResourceClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "*BadName!"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||||
|
test.TestUpdate(
|
||||||
|
// valid
|
||||||
|
validNewClass("foo"),
|
||||||
|
// updateFunc
|
||||||
|
func(obj runtime.Object) runtime.Object {
|
||||||
|
object := obj.(*resource.ResourceClass)
|
||||||
|
object.ParametersRef = &resource.ResourceClassParametersReference{Kind: "cdiexample", Name: "some-name"}
|
||||||
|
return object
|
||||||
|
},
|
||||||
|
//invalid update
|
||||||
|
func(obj runtime.Object) runtime.Object {
|
||||||
|
object := obj.(*resource.ResourceClass)
|
||||||
|
object.DriverName = ""
|
||||||
|
return object
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject()
|
||||||
|
test.TestDelete(validNewClass("foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||||
|
test.TestGet(validNewClass("foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||||
|
test.TestList(validNewClass("foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
storage, server := newStorage(t)
|
||||||
|
defer server.Terminate(t)
|
||||||
|
defer storage.Store.DestroyFunc()
|
||||||
|
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||||
|
test.TestWatch(
|
||||||
|
validNewClass("foo"),
|
||||||
|
// matching labels
|
||||||
|
[]labels.Set{},
|
||||||
|
// not matching labels
|
||||||
|
[]labels.Set{
|
||||||
|
{"foo": "bar"},
|
||||||
|
},
|
||||||
|
// matching fields
|
||||||
|
[]fields.Set{
|
||||||
|
{"metadata.name": "foo"},
|
||||||
|
},
|
||||||
|
// not matching fields
|
||||||
|
[]fields.Set{
|
||||||
|
{"metadata.name": "bar"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
75
pkg/registry/resource/resourceclass/strategy.go
Normal file
75
pkg/registry/resource/resourceclass/strategy.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 resourceclass
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resourceClassStrategy implements behavior for ResourceClass objects
|
||||||
|
type resourceClassStrategy struct {
|
||||||
|
runtime.ObjectTyper
|
||||||
|
names.NameGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
var Strategy = resourceClassStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) NamespaceScoped() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||||
|
resourceClass := obj.(*resource.ResourceClass)
|
||||||
|
return validation.ValidateClass(resourceClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) Canonicalize(obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) AllowCreateOnUpdate() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
|
errorList := validation.ValidateClass(obj.(*resource.ResourceClass))
|
||||||
|
return append(errorList, validation.ValidateClassUpdate(obj.(*resource.ResourceClass), old.(*resource.ResourceClass))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resourceClassStrategy) AllowUnconditionalUpdate() bool {
|
||||||
|
return true
|
||||||
|
}
|
81
pkg/registry/resource/resourceclass/strategy_test.go
Normal file
81
pkg/registry/resource/resourceclass/strategy_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 resourceclass
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resourceClass = &resource.ResourceClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "valid-class",
|
||||||
|
},
|
||||||
|
DriverName: "resource-driver.example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassStrategy(t *testing.T) {
|
||||||
|
if Strategy.NamespaceScoped() {
|
||||||
|
t.Errorf("ResourceClass must not be namespace scoped")
|
||||||
|
}
|
||||||
|
if Strategy.AllowCreateOnUpdate() {
|
||||||
|
t.Errorf("ResourceClass should not allow create on update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassStrategyCreate(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClass := resourceClass.DeepCopy()
|
||||||
|
|
||||||
|
Strategy.PrepareForCreate(ctx, resourceClass)
|
||||||
|
errs := Strategy.Validate(ctx, resourceClass)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("unexpected error validating for create %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassStrategyUpdate(t *testing.T) {
|
||||||
|
t.Run("no-changes-okay", func(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClass := resourceClass.DeepCopy()
|
||||||
|
newClass := resourceClass.DeepCopy()
|
||||||
|
newClass.ResourceVersion = "4"
|
||||||
|
|
||||||
|
Strategy.PrepareForUpdate(ctx, newClass, resourceClass)
|
||||||
|
errs := Strategy.ValidateUpdate(ctx, newClass, resourceClass)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("unexpected validation errors: %v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("name-change-not-allowed", func(t *testing.T) {
|
||||||
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
|
resourceClass := resourceClass.DeepCopy()
|
||||||
|
newClass := resourceClass.DeepCopy()
|
||||||
|
newClass.Name = "valid-class-2"
|
||||||
|
newClass.ResourceVersion = "4"
|
||||||
|
|
||||||
|
Strategy.PrepareForUpdate(ctx, newClass, resourceClass)
|
||||||
|
errs := Strategy.ValidateUpdate(ctx, newClass, resourceClass)
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Errorf("expected a validation error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
91
pkg/registry/resource/rest/storage_resource.go
Normal file
91
pkg/registry/resource/rest/storage_resource.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
resourcev1alpha1 "k8s.io/api/resource/v1alpha1"
|
||||||
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
|
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||||
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/resource"
|
||||||
|
podschedulingstore "k8s.io/kubernetes/pkg/registry/resource/podscheduling/storage"
|
||||||
|
resourceclaimstore "k8s.io/kubernetes/pkg/registry/resource/resourceclaim/storage"
|
||||||
|
resourceclaimtemplatestore "k8s.io/kubernetes/pkg/registry/resource/resourceclaimtemplate/storage"
|
||||||
|
resourceclassstore "k8s.io/kubernetes/pkg/registry/resource/resourceclass/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RESTStorageProvider struct{}
|
||||||
|
|
||||||
|
func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error) {
|
||||||
|
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(resource.GroupName, legacyscheme.Scheme, legacyscheme.ParameterCodec, legacyscheme.Codecs)
|
||||||
|
// If you add a version here, be sure to add an entry in `k8s.io/kubernetes/cmd/kube-apiserver/app/aggregator.go with specific priorities.
|
||||||
|
// TODO refactor the plumbing to provide the information in the APIGroupInfo
|
||||||
|
|
||||||
|
if storageMap, err := p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter); err != nil {
|
||||||
|
return genericapiserver.APIGroupInfo{}, err
|
||||||
|
} else if len(storageMap) > 0 {
|
||||||
|
apiGroupInfo.VersionedResourcesStorageMap[resourcev1alpha1.SchemeGroupVersion.Version] = storageMap
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiGroupInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) {
|
||||||
|
storage := map[string]rest.Storage{}
|
||||||
|
|
||||||
|
if resource := "resourceclasses"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha1.SchemeGroupVersion.WithResource(resource)) {
|
||||||
|
resourceClassStorage, err := resourceclassstore.NewREST(restOptionsGetter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
storage[resource] = resourceClassStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
if resource := "resourceclaims"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha1.SchemeGroupVersion.WithResource(resource)) {
|
||||||
|
resourceClaimStorage, resourceClaimStatusStorage, err := resourceclaimstore.NewREST(restOptionsGetter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
storage[resource] = resourceClaimStorage
|
||||||
|
storage[resource+"/status"] = resourceClaimStatusStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
if resource := "resourceclaimtemplates"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha1.SchemeGroupVersion.WithResource(resource)) {
|
||||||
|
resourceClaimTemplateStorage, err := resourceclaimtemplatestore.NewREST(restOptionsGetter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
storage[resource] = resourceClaimTemplateStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
if resource := "podschedulings"; apiResourceConfigSource.ResourceEnabled(resourcev1alpha1.SchemeGroupVersion.WithResource(resource)) {
|
||||||
|
podSchedulingStorage, podSchedulingStatusStorage, err := podschedulingstore.NewREST(restOptionsGetter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
storage[resource] = podSchedulingStorage
|
||||||
|
storage[resource+"/status"] = podSchedulingStatusStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p RESTStorageProvider) GroupName() string {
|
||||||
|
return resource.GroupName
|
||||||
|
}
|
6
staging/src/k8s.io/api/resource/OWNERS
Normal file
6
staging/src/k8s.io/api/resource/OWNERS
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# See the OWNERS docs at https://go.k8s.io/owners
|
||||||
|
|
||||||
|
reviewers:
|
||||||
|
- bart0sh
|
||||||
|
- klueska
|
||||||
|
- pohly
|
24
staging/src/k8s.io/api/resource/v1alpha1/doc.go
Normal file
24
staging/src/k8s.io/api/resource/v1alpha1/doc.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
// +k8s:deepcopy-gen=package
|
||||||
|
// +k8s:protobuf-gen=package
|
||||||
|
|
||||||
|
// +groupName=resource.k8s.io
|
||||||
|
|
||||||
|
// Package v1alpha1 is the v1alpha1 version of the resource API.
|
||||||
|
package v1alpha1 // import "k8s.io/api/resource/v1alpha1"
|
63
staging/src/k8s.io/api/resource/v1alpha1/register.go
Normal file
63
staging/src/k8s.io/api/resource/v1alpha1/register.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroupName is the group name use in this package
|
||||||
|
const GroupName = "resource.k8s.io"
|
||||||
|
|
||||||
|
// SchemeGroupVersion is group version used to register these objects
|
||||||
|
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||||
|
|
||||||
|
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||||
|
func Resource(resource string) schema.GroupResource {
|
||||||
|
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// We only register manually written functions here. The registration of the
|
||||||
|
// generated functions takes place in the generated files. The separation
|
||||||
|
// makes the code compile even when the generated files are missing.
|
||||||
|
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||||
|
AddToScheme = SchemeBuilder.AddToScheme
|
||||||
|
)
|
||||||
|
|
||||||
|
// Adds the list of known types to the given scheme.
|
||||||
|
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||||
|
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||||
|
&ResourceClass{},
|
||||||
|
&ResourceClassList{},
|
||||||
|
&ResourceClaim{},
|
||||||
|
&ResourceClaimList{},
|
||||||
|
&ResourceClaimTemplate{},
|
||||||
|
&ResourceClaimTemplateList{},
|
||||||
|
&PodScheduling{},
|
||||||
|
&PodSchedulingList{},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add common types
|
||||||
|
scheme.AddKnownTypes(SchemeGroupVersion, &metav1.Status{})
|
||||||
|
|
||||||
|
// Add the watch version that applies
|
||||||
|
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||||
|
return nil
|
||||||
|
}
|
429
staging/src/k8s.io/api/resource/v1alpha1/types.go
Normal file
429
staging/src/k8s.io/api/resource/v1alpha1/types.go
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// +genclient
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||||
|
|
||||||
|
// ResourceClaim describes which resources are needed by a resource consumer.
|
||||||
|
// Its status tracks whether the resource has been allocated and what the
|
||||||
|
// resulting attributes are.
|
||||||
|
//
|
||||||
|
// This is an alpha type and requires enabling the DynamicResourceAllocation
|
||||||
|
// feature gate.
|
||||||
|
type ResourceClaim struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
// Standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// Spec describes the desired attributes of a resource that then needs
|
||||||
|
// to be allocated. It can only be set once when creating the
|
||||||
|
// ResourceClaim.
|
||||||
|
Spec ResourceClaimSpec `json:"spec" protobuf:"bytes,2,name=spec"`
|
||||||
|
|
||||||
|
// Status describes whether the resource is available and with which
|
||||||
|
// attributes.
|
||||||
|
// +optional
|
||||||
|
Status ResourceClaimStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimSpec defines how a resource is to be allocated.
|
||||||
|
type ResourceClaimSpec struct {
|
||||||
|
// ResourceClassName references the driver and additional parameters
|
||||||
|
// via the name of a ResourceClass that was created as part of the
|
||||||
|
// driver deployment.
|
||||||
|
ResourceClassName string `json:"resourceClassName" protobuf:"bytes,1,name=resourceClassName"`
|
||||||
|
|
||||||
|
// ParametersRef references a separate object with arbitrary parameters
|
||||||
|
// that will be used by the driver when allocating a resource for the
|
||||||
|
// claim.
|
||||||
|
//
|
||||||
|
// The object must be in the same namespace as the ResourceClaim.
|
||||||
|
// +optional
|
||||||
|
ParametersRef *ResourceClaimParametersReference `json:"parametersRef,omitempty" protobuf:"bytes,2,opt,name=parametersRef"`
|
||||||
|
|
||||||
|
// Allocation can start immediately or when a Pod wants to use the
|
||||||
|
// resource. "WaitForFirstConsumer" is the default.
|
||||||
|
// +optional
|
||||||
|
AllocationMode AllocationMode `json:"allocationMode,omitempty" protobuf:"bytes,3,opt,name=allocationMode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllocationMode describes whether a ResourceClaim gets allocated immediately
|
||||||
|
// when it gets created (AllocationModeImmediate) or whether allocation is
|
||||||
|
// delayed until it is needed for a Pod
|
||||||
|
// (AllocationModeWaitForFirstConsumer). Other modes might get added in the
|
||||||
|
// future.
|
||||||
|
type AllocationMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// When a ResourceClaim has AllocationModeWaitForFirstConsumer, allocation is
|
||||||
|
// delayed until a Pod gets scheduled that needs the ResourceClaim. The
|
||||||
|
// scheduler will consider all resource requirements of that Pod and
|
||||||
|
// trigger allocation for a node that fits the Pod.
|
||||||
|
AllocationModeWaitForFirstConsumer AllocationMode = "WaitForFirstConsumer"
|
||||||
|
|
||||||
|
// When a ResourceClaim has AllocationModeImmediate, allocation starts
|
||||||
|
// as soon as the ResourceClaim gets created. This is done without
|
||||||
|
// considering the needs of Pods that will use the ResourceClaim
|
||||||
|
// because those Pods are not known yet.
|
||||||
|
AllocationModeImmediate AllocationMode = "Immediate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResourceClaimStatus tracks whether the resource has been allocated and what
|
||||||
|
// the resulting attributes are.
|
||||||
|
type ResourceClaimStatus struct {
|
||||||
|
// DriverName is a copy of the driver name from the ResourceClass at
|
||||||
|
// the time when allocation started.
|
||||||
|
// +optional
|
||||||
|
DriverName string `json:"driverName,omitempty" protobuf:"bytes,1,opt,name=driverName"`
|
||||||
|
|
||||||
|
// Allocation is set by the resource driver once a resource has been
|
||||||
|
// allocated successfully. If this is not specified, the resource is
|
||||||
|
// not yet allocated.
|
||||||
|
// +optional
|
||||||
|
Allocation *AllocationResult `json:"allocation,omitempty" protobuf:"bytes,2,opt,name=allocation"`
|
||||||
|
|
||||||
|
// ReservedFor indicates which entities are currently allowed to use
|
||||||
|
// the claim. A Pod which references a ResourceClaim which is not
|
||||||
|
// reserved for that Pod will not be started.
|
||||||
|
//
|
||||||
|
// There can be at most 32 such reservations. This may get increased in
|
||||||
|
// the future, but not reduced.
|
||||||
|
//
|
||||||
|
// +listType=set
|
||||||
|
// +optional
|
||||||
|
ReservedFor []ResourceClaimConsumerReference `json:"reservedFor,omitempty" protobuf:"bytes,3,opt,name=reservedFor"`
|
||||||
|
|
||||||
|
// DeallocationRequested indicates that a ResourceClaim is to be
|
||||||
|
// deallocated.
|
||||||
|
//
|
||||||
|
// The driver then must deallocate this claim and reset the field
|
||||||
|
// together with clearing the Allocation field.
|
||||||
|
//
|
||||||
|
// While DeallocationRequested is set, no new consumers may be added to
|
||||||
|
// ReservedFor.
|
||||||
|
// +optional
|
||||||
|
DeallocationRequested bool `json:"deallocationRequested,omitempty" protobuf:"varint,4,opt,name=deallocationRequested"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReservedForMaxSize is the maximum number of entries in
|
||||||
|
// claim.status.reservedFor.
|
||||||
|
const ResourceClaimReservedForMaxSize = 32
|
||||||
|
|
||||||
|
// AllocationResult contains attributed of an allocated resource.
|
||||||
|
type AllocationResult struct {
|
||||||
|
// ResourceHandle contains arbitrary data returned by the driver after a
|
||||||
|
// successful allocation. This is opaque for
|
||||||
|
// Kubernetes. Driver documentation may explain to users how to
|
||||||
|
// interpret this data if needed.
|
||||||
|
//
|
||||||
|
// The maximum size of this field is 16KiB. This may get
|
||||||
|
// increased in the future, but not reduced.
|
||||||
|
// +optional
|
||||||
|
ResourceHandle string `json:"resourceHandle,omitempty" protobuf:"bytes,1,opt,name=resourceHandle"`
|
||||||
|
|
||||||
|
// This field will get set by the resource driver after it has
|
||||||
|
// allocated the resource driver to inform the scheduler where it can
|
||||||
|
// schedule Pods using the ResourceClaim.
|
||||||
|
//
|
||||||
|
// Setting this field is optional. If null, the resource is available
|
||||||
|
// everywhere.
|
||||||
|
// +optional
|
||||||
|
AvailableOnNodes *v1.NodeSelector `json:"availableOnNodes,omitempty" protobuf:"bytes,2,opt,name=availableOnNodes"`
|
||||||
|
|
||||||
|
// Shareable determines whether the resource supports more
|
||||||
|
// than one consumer at a time.
|
||||||
|
// +optional
|
||||||
|
Shareable bool `json:"shareable,omitempty" protobuf:"varint,3,opt,name=shareable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceHandleMaxSize is the maximum size of allocation.resourceHandle.
|
||||||
|
const ResourceHandleMaxSize = 16 * 1024
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||||
|
|
||||||
|
// ResourceClaimList is a collection of claims.
|
||||||
|
type ResourceClaimList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
// Standard list metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// Items is the list of resource claims.
|
||||||
|
Items []ResourceClaim `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +genclient
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||||
|
|
||||||
|
// PodScheduling objects hold information that is needed to schedule
|
||||||
|
// a Pod with ResourceClaims that use "WaitForFirstConsumer" allocation
|
||||||
|
// mode.
|
||||||
|
//
|
||||||
|
// This is an alpha type and requires enabling the DynamicResourceAllocation
|
||||||
|
// feature gate.
|
||||||
|
type PodScheduling struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
// Standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// Spec describes where resources for the Pod are needed.
|
||||||
|
Spec PodSchedulingSpec `json:"spec" protobuf:"bytes,2,name=spec"`
|
||||||
|
|
||||||
|
// Status describes where resources for the Pod can be allocated.
|
||||||
|
// +optional
|
||||||
|
Status PodSchedulingStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodSchedulingSpec describes where resources for the Pod are needed.
|
||||||
|
type PodSchedulingSpec struct {
|
||||||
|
// SelectedNode is the node for which allocation of ResourceClaims that
|
||||||
|
// are referenced by the Pod and that use "WaitForFirstConsumer"
|
||||||
|
// allocation is to be attempted.
|
||||||
|
// +optional
|
||||||
|
SelectedNode string `json:"selectedNode,omitempty" protobuf:"bytes,1,opt,name=selectedNode"`
|
||||||
|
|
||||||
|
// PotentialNodes lists nodes where the Pod might be able to run.
|
||||||
|
//
|
||||||
|
// The size of this field is limited to 128. This is large enough for
|
||||||
|
// many clusters. Larger clusters may need more attempts to find a node
|
||||||
|
// that suits all pending resources. This may get increased in the
|
||||||
|
// future, but not reduced.
|
||||||
|
//
|
||||||
|
// +listType=set
|
||||||
|
// +optional
|
||||||
|
PotentialNodes []string `json:"potentialNodes,omitempty" protobuf:"bytes,2,opt,name=potentialNodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodSchedulingStatus describes where resources for the Pod can be allocated.
|
||||||
|
type PodSchedulingStatus struct {
|
||||||
|
// ResourceClaims describes resource availability for each
|
||||||
|
// pod.spec.resourceClaim entry where the corresponding ResourceClaim
|
||||||
|
// uses "WaitForFirstConsumer" allocation mode.
|
||||||
|
//
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=name
|
||||||
|
// +optional
|
||||||
|
ResourceClaims []ResourceClaimSchedulingStatus `json:"resourceClaims,omitempty" protobuf:"bytes,1,opt,name=resourceClaims"`
|
||||||
|
|
||||||
|
// If there ever is a need to support other kinds of resources
|
||||||
|
// than ResourceClaim, then new fields could get added here
|
||||||
|
// for those other resources.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimSchedulingStatus contains information about one particular
|
||||||
|
// ResourceClaim with "WaitForFirstConsumer" allocation mode.
|
||||||
|
type ResourceClaimSchedulingStatus struct {
|
||||||
|
// Name matches the pod.spec.resourceClaims[*].Name field.
|
||||||
|
// +optional
|
||||||
|
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`
|
||||||
|
|
||||||
|
// UnsuitableNodes lists nodes that the ResourceClaim cannot be
|
||||||
|
// allocated for.
|
||||||
|
//
|
||||||
|
// The size of this field is limited to 128, the same as for
|
||||||
|
// PodSchedulingSpec.PotentialNodes. This may get increased in the
|
||||||
|
// future, but not reduced.
|
||||||
|
//
|
||||||
|
// +listType=set
|
||||||
|
// +optional
|
||||||
|
UnsuitableNodes []string `json:"unsuitableNodes,omitempty" protobuf:"bytes,2,opt,name=unsuitableNodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodSchedulingNodeListMaxSize defines the maximum number of entries in the
|
||||||
|
// node lists that are stored in PodScheduling objects. This limit is part
|
||||||
|
// of the API.
|
||||||
|
const PodSchedulingNodeListMaxSize = 128
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||||
|
|
||||||
|
// PodSchedulingList is a collection of Pod scheduling objects.
|
||||||
|
type PodSchedulingList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
// Standard list metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// Items is the list of PodScheduling objects.
|
||||||
|
Items []PodScheduling `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +genclient
|
||||||
|
// +genclient:nonNamespaced
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||||
|
|
||||||
|
// ResourceClass is used by administrators to influence how resources
|
||||||
|
// are allocated.
|
||||||
|
//
|
||||||
|
// This is an alpha type and requires enabling the DynamicResourceAllocation
|
||||||
|
// feature gate.
|
||||||
|
type ResourceClass struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
// Standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// DriverName defines the name of the dynamic resource driver that is
|
||||||
|
// used for allocation of a ResourceClaim that uses this class.
|
||||||
|
//
|
||||||
|
// Resource drivers have a unique name in forward domain order
|
||||||
|
// (acme.example.com).
|
||||||
|
DriverName string `json:"driverName" protobuf:"bytes,2,name=driverName"`
|
||||||
|
|
||||||
|
// ParametersRef references an arbitrary separate object that may hold
|
||||||
|
// parameters that will be used by the driver when allocating a
|
||||||
|
// resource that uses this class. A dynamic resource driver can
|
||||||
|
// distinguish between parameters stored here and and those stored in
|
||||||
|
// ResourceClaimSpec.
|
||||||
|
// +optional
|
||||||
|
ParametersRef *ResourceClassParametersReference `json:"parametersRef,omitempty" protobuf:"bytes,3,opt,name=parametersRef"`
|
||||||
|
|
||||||
|
// Only nodes matching the selector will be considered by the scheduler
|
||||||
|
// when trying to find a Node that fits a Pod when that Pod uses
|
||||||
|
// a ResourceClaim that has not been allocated yet.
|
||||||
|
//
|
||||||
|
// Setting this field is optional. If null, all nodes are candidates.
|
||||||
|
// +optional
|
||||||
|
SuitableNodes *v1.NodeSelector `json:"suitableNodes,omitempty" protobuf:"bytes,4,opt,name=suitableNodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||||
|
|
||||||
|
// ResourceClassList is a collection of classes.
|
||||||
|
type ResourceClassList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
// Standard list metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// Items is the list of resource classes.
|
||||||
|
Items []ResourceClass `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClassParametersReference contains enough information to let you
|
||||||
|
// locate the parameters for a ResourceClass.
|
||||||
|
type ResourceClassParametersReference struct {
|
||||||
|
// APIGroup is the group for the resource being referenced. It is
|
||||||
|
// empty for the core API. This matches the group in the APIVersion
|
||||||
|
// that is used when creating the resources.
|
||||||
|
// +optional
|
||||||
|
APIGroup string `json:"apiGroup,omitempty" protobuf:"bytes,1,opt,name=apiGroup"`
|
||||||
|
// Kind is the type of resource being referenced. This is the same
|
||||||
|
// value as in the parameter object's metadata.
|
||||||
|
Kind string `json:"kind" protobuf:"bytes,2,name=kind"`
|
||||||
|
// Name is the name of resource being referenced.
|
||||||
|
Name string `json:"name" protobuf:"bytes,3,name=name"`
|
||||||
|
// Namespace that contains the referenced resource. Must be empty
|
||||||
|
// for cluster-scoped resources and non-empty for namespaced
|
||||||
|
// resources.
|
||||||
|
// +optional
|
||||||
|
Namespace string `json:"namespace,omitempty" protobuf:"bytes,4,opt,name=namespace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimParametersReference contains enough information to let you
|
||||||
|
// locate the parameters for a ResourceClaim. The object must be in the same
|
||||||
|
// namespace as the ResourceClaim.
|
||||||
|
type ResourceClaimParametersReference struct {
|
||||||
|
// APIGroup is the group for the resource being referenced. It is
|
||||||
|
// empty for the core API. This matches the group in the APIVersion
|
||||||
|
// that is used when creating the resources.
|
||||||
|
// +optional
|
||||||
|
APIGroup string `json:"apiGroup,omitempty" protobuf:"bytes,1,opt,name=apiGroup"`
|
||||||
|
// Kind is the type of resource being referenced. This is the same
|
||||||
|
// value as in the parameter object's metadata, for example "ConfigMap".
|
||||||
|
Kind string `json:"kind" protobuf:"bytes,2,name=kind"`
|
||||||
|
// Name is the name of resource being referenced.
|
||||||
|
Name string `json:"name" protobuf:"bytes,3,name=name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimConsumerReference contains enough information to let you
|
||||||
|
// locate the consumer of a ResourceClaim. The user must be a resource in the same
|
||||||
|
// namespace as the ResourceClaim.
|
||||||
|
type ResourceClaimConsumerReference struct {
|
||||||
|
// APIGroup is the group for the resource being referenced. It is
|
||||||
|
// empty for the core API. This matches the group in the APIVersion
|
||||||
|
// that is used when creating the resources.
|
||||||
|
// +optional
|
||||||
|
APIGroup string `json:"apiGroup,omitempty" protobuf:"bytes,1,opt,name=apiGroup"`
|
||||||
|
// Resource is the type of resource being referenced, for example "pods".
|
||||||
|
Resource string `json:"resource" protobuf:"bytes,3,name=resource"`
|
||||||
|
// Name is the name of resource being referenced.
|
||||||
|
Name string `json:"name" protobuf:"bytes,4,name=name"`
|
||||||
|
// UID identifies exactly one incarnation of the resource.
|
||||||
|
UID types.UID `json:"uid" protobuf:"bytes,5,name=uid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +genclient
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||||
|
|
||||||
|
// ResourceClaimTemplate is used to produce ResourceClaim objects.
|
||||||
|
type ResourceClaimTemplate struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
// Standard object metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// Describes the ResourceClaim that is to be generated.
|
||||||
|
//
|
||||||
|
// This field is immutable. A ResourceClaim will get created by the
|
||||||
|
// control plane for a Pod when needed and then not get updated
|
||||||
|
// anymore.
|
||||||
|
Spec ResourceClaimTemplateSpec `json:"spec" protobuf:"bytes,2,name=spec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceClaimTemplateSpec contains the metadata and fields for a ResourceClaim.
|
||||||
|
type ResourceClaimTemplateSpec struct {
|
||||||
|
// ObjectMeta may contain labels and annotations that will be copied into the PVC
|
||||||
|
// when creating it. No other fields are allowed and will be rejected during
|
||||||
|
// validation.
|
||||||
|
// +optional
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// Spec for the ResourceClaim. The entire content is copied unchanged
|
||||||
|
// into the ResourceClaim that gets created from this template. The
|
||||||
|
// same fields as in a ResourceClaim are also valid here.
|
||||||
|
Spec ResourceClaimSpec `json:"spec" protobuf:"bytes,2,name=spec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
// +k8s:prerelease-lifecycle-gen:introduced=1.26
|
||||||
|
|
||||||
|
// ResourceClaimTemplateList is a collection of claim templates.
|
||||||
|
type ResourceClaimTemplateList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
// Standard list metadata
|
||||||
|
// +optional
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
|
||||||
|
|
||||||
|
// Items is the list of resource claim templates.
|
||||||
|
Items []ResourceClaimTemplate `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||||
|
}
|
@ -66,6 +66,7 @@ import (
|
|||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
rbacv1alpha1 "k8s.io/api/rbac/v1alpha1"
|
rbacv1alpha1 "k8s.io/api/rbac/v1alpha1"
|
||||||
rbacv1beta1 "k8s.io/api/rbac/v1beta1"
|
rbacv1beta1 "k8s.io/api/rbac/v1beta1"
|
||||||
|
resourcev1alpha1 "k8s.io/api/resource/v1alpha1"
|
||||||
schedulingv1 "k8s.io/api/scheduling/v1"
|
schedulingv1 "k8s.io/api/scheduling/v1"
|
||||||
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
|
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
|
||||||
schedulingv1beta1 "k8s.io/api/scheduling/v1beta1"
|
schedulingv1beta1 "k8s.io/api/scheduling/v1beta1"
|
||||||
@ -128,6 +129,7 @@ var groups = []runtime.SchemeBuilder{
|
|||||||
rbacv1alpha1.SchemeBuilder,
|
rbacv1alpha1.SchemeBuilder,
|
||||||
rbacv1beta1.SchemeBuilder,
|
rbacv1beta1.SchemeBuilder,
|
||||||
rbacv1.SchemeBuilder,
|
rbacv1.SchemeBuilder,
|
||||||
|
resourcev1alpha1.SchemeBuilder,
|
||||||
schedulingv1alpha1.SchemeBuilder,
|
schedulingv1alpha1.SchemeBuilder,
|
||||||
schedulingv1beta1.SchemeBuilder,
|
schedulingv1beta1.SchemeBuilder,
|
||||||
schedulingv1.SchemeBuilder,
|
schedulingv1.SchemeBuilder,
|
||||||
|
@ -64,6 +64,8 @@ var resetFieldsStatusData = map[schema.GroupVersionResource]string{
|
|||||||
gvr("storage.k8s.io", "v1", "volumeattachments"): `{"status": {"attached": false}}`,
|
gvr("storage.k8s.io", "v1", "volumeattachments"): `{"status": {"attached": false}}`,
|
||||||
gvr("policy", "v1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
|
gvr("policy", "v1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
|
||||||
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
|
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "podschedulings"): `{"status": {"resourceClaims": [{"name": "my-claim", "unsuitableNodes": ["node2"]}]}}`, // Not really a conflict with status_test.go: Apply just stores both nodes. Conflict testing therefore gets disabled for podschedulings.
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "resourceclaims"): `{"status": {"driverName": "other.example.com"}}`,
|
||||||
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
|
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +86,10 @@ var noConflicts = map[string]struct{}{
|
|||||||
// namespaces only have a spec.finalizers field which is also skipped,
|
// namespaces only have a spec.finalizers field which is also skipped,
|
||||||
// thus it will never have a conflict.
|
// thus it will never have a conflict.
|
||||||
"namespaces": {},
|
"namespaces": {},
|
||||||
|
// podschedulings.status only has a list which contains items with a list,
|
||||||
|
// therefore apply works because it simply merges either the outer or
|
||||||
|
// the inner list.
|
||||||
|
"podschedulings": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
var image2 = image.GetE2EImage(image.Etcd)
|
var image2 = image.GetE2EImage(image.Etcd)
|
||||||
@ -140,6 +146,10 @@ var resetFieldsSpecData = map[schema.GroupVersionResource]string{
|
|||||||
gvr("awesome.bears.com", "v3", "pandas"): `{"spec": {"replicas": 302}}`,
|
gvr("awesome.bears.com", "v3", "pandas"): `{"spec": {"replicas": 302}}`,
|
||||||
gvr("apiregistration.k8s.io", "v1beta1", "apiservices"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"group": "foo2.com"}}`,
|
gvr("apiregistration.k8s.io", "v1beta1", "apiservices"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"group": "foo2.com"}}`,
|
||||||
gvr("apiregistration.k8s.io", "v1", "apiservices"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"group": "foo2.com"}}`,
|
gvr("apiregistration.k8s.io", "v1", "apiservices"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"group": "foo2.com"}}`,
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "podschedulings"): `{"spec": {"selectedNode": "node2name"}}`,
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "resourceclasses"): `{"driverName": "other.example.com"}`,
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "resourceclaims"): `{"spec": {"resourceClassName": "class2name"}}`, // ResourceClassName is immutable, but that doesn't matter for the test.
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "resourceclaimtemplates"): `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
|
||||||
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{}`,
|
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,8 @@ var statusData = map[schema.GroupVersionResource]string{
|
|||||||
gvr("storage.k8s.io", "v1", "volumeattachments"): `{"status": {"attached": true}}`,
|
gvr("storage.k8s.io", "v1", "volumeattachments"): `{"status": {"attached": true}}`,
|
||||||
gvr("policy", "v1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`,
|
gvr("policy", "v1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`,
|
||||||
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`,
|
gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 5}}`,
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "podschedulings"): `{"status": {"resourceClaims": [{"name": "my-claim", "unsuitableNodes": ["node1"]}]}}`,
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "resourceclaims"): `{"status": {"driverName": "example.com"}}`,
|
||||||
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
|
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,6 +459,25 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes
|
|||||||
},
|
},
|
||||||
// --
|
// --
|
||||||
|
|
||||||
|
// k8s.io/kubernetes/pkg/apis/resource/v1alpha1
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "resourceclasses"): {
|
||||||
|
Stub: `{"metadata": {"name": "class1name"}, "driverName": "example.com"}`,
|
||||||
|
ExpectedEtcdPath: "/registry/resourceclasses/class1name",
|
||||||
|
},
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "resourceclaims"): {
|
||||||
|
Stub: `{"metadata": {"name": "claim1name"}, "spec": {"resourceClassName": "class1name", "allocationMode": "WaitForFirstConsumer"}}`,
|
||||||
|
ExpectedEtcdPath: "/registry/resourceclaims/" + namespace + "/claim1name",
|
||||||
|
},
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "resourceclaimtemplates"): {
|
||||||
|
Stub: `{"metadata": {"name": "claimtemplate1name"}, "spec": {"spec": {"resourceClassName": "class1name", "allocationMode": "WaitForFirstConsumer"}}}`,
|
||||||
|
ExpectedEtcdPath: "/registry/resourceclaimtemplates/" + namespace + "/claimtemplate1name",
|
||||||
|
},
|
||||||
|
gvr("resource.k8s.io", "v1alpha1", "podschedulings"): {
|
||||||
|
Stub: `{"metadata": {"name": "pod1name"}, "spec": {"selectedNode": "node1name", "potentialNodes": ["node1name", "node2name"]}}`,
|
||||||
|
ExpectedEtcdPath: "/registry/podschedulings/" + namespace + "/pod1name",
|
||||||
|
},
|
||||||
|
// --
|
||||||
|
|
||||||
// k8s.io/apiserver/pkg/apis/apiserverinternal/v1alpha1
|
// k8s.io/apiserver/pkg/apis/apiserverinternal/v1alpha1
|
||||||
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): {
|
gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): {
|
||||||
Stub: `{"metadata":{"name":"sv1.test"},"spec":{}}`,
|
Stub: `{"metadata":{"name":"sv1.test"},"spec":{}}`,
|
||||||
|
Loading…
Reference in New Issue
Block a user