diff --git a/pkg/api/types.go b/pkg/api/types.go index 770aca78a90..31fd337b8cd 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -138,6 +138,16 @@ type ObjectMeta struct { // objects. Annotation keys have the same formatting restrictions as Label keys. See the // comments on Labels for details. Annotations map[string]string `json:"annotations,omitempty"` + + // List of objects depended by this object. If ALL objects in the list have + // been deleted, this object will be garbage collected. + OwnerReferences []OwnerReference `json:"ownerReferences,omitempty"` + + // Must be empty before the object is deleted from the registry. Each entry + // is an identifier for the responsible component that will remove the entry + // from the list. If the deletionTimestamp of the object is non-nil, entries + // in this list can only be removed. + Finalizers []string `json:"finalizers,omitempty"` } const ( @@ -1925,6 +1935,10 @@ type DeleteOptions struct { // Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be // returned. Preconditions *Preconditions `json:"preconditions,omitempty"` + + // Should the dependent objects be orphaned. If true/false, the "orphan" + // finalizer will be added to/removed from the object's finalizers list. + OrphanDependents *bool `json:"orphanDependents,omitempty"` } // ExportOptions is the query options to the standard REST get call. @@ -2057,6 +2071,23 @@ type ServiceProxyOptions struct { Path string } +// OwnerReference contains enough information to let you identify an owning +// object. Currently, an owning object must be in the same namespace, so there +// is no namespace field. +type OwnerReference struct { + // API version of the referent. + APIVersion string `json:"apiVersion"` + // Kind of the referent. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds + Kind string `json:"kind"` + // Name of the referent. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/identifiers.md#names + Name string `json:"name"` + // UID of the referent. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/identifiers.md#uids + UID types.UID `json:"uid"` +} + // ObjectReference contains enough information to let you inspect or modify the referred object. type ObjectReference struct { Kind string `json:"kind,omitempty"` diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 75c780a8983..54cdcb57dc2 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -175,6 +175,16 @@ type ObjectMeta struct { // queryable and should be preserved when modifying objects. // More info: http://releases.k8s.io/HEAD/docs/user-guide/annotations.md Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,12,rep,name=annotations"` + + // List of objects depended by this object. If ALL objects in the list have + // been deleted, this object will be garbage collected. + OwnerReferences []OwnerReference `json:"ownerReferences,omitempty" patchStrategy:"merge" patchMergeKey:"uid"` + + // Must be empty before the object is deleted from the registry. Each entry + // is an identifier for the responsible component that will remove the entry + // from the list. If the deletionTimestamp of the object is non-nil, entries + // in this list can only be removed. + Finalizers []string `json:"finalizers,omitempty"` } const ( @@ -2331,6 +2341,10 @@ type DeleteOptions struct { // Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be // returned. Preconditions *Preconditions `json:"preconditions,omitempty" protobuf:"bytes,2,opt,name=preconditions"` + + // Should the dependent objects be orphaned. If true/false, the "orphan" + // finalizer will be added to/removed from the object's finalizers list. + OrphanDependents *bool `json:"orphanDependents,omitempty"` } // ExportOptions is the query options to the standard REST get call. @@ -2484,6 +2498,23 @@ type ServiceProxyOptions struct { Path string `json:"path,omitempty" protobuf:"bytes,1,opt,name=path"` } +// OwnerReference contains enough information to let you identify an owning +// object. Currently, an owning object must be in the same namespace, so there +// is no namespace field. +type OwnerReference struct { + // API version of the referent. + APIVersion string `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"` + // Kind of the referent. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds + Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"` + // Name of the referent. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/identifiers.md#names + Name string `json:"name" protobuf:"bytes,3,opt,name=name"` + // UID of the referent. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/identifiers.md#uids + UID types.UID `json:"uid" protobuf:"bytes,4,opt,name=uid,casttype=k8s.io/kubernetes/pkg/types.UID"` +} + // ObjectReference contains enough information to let you inspect or modify the referred object. type ObjectReference struct { // Kind of the referent. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 309721701b5..95d729cb2a5 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -33,6 +33,8 @@ import ( utilpod "k8s.io/kubernetes/pkg/api/pod" "k8s.io/kubernetes/pkg/api/resource" apiservice "k8s.io/kubernetes/pkg/api/service" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/capabilities" "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/util/intstr" @@ -67,6 +69,11 @@ var PortNameErrorMsg string = fmt.Sprintf(`must be an IANA_SVC_NAME (at most 15 const totalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB +// BannedOwners is a black list of object that are not allowed to be owners. +var BannedOwners = map[unversioned.GroupVersionKind]struct{}{ + v1.SchemeGroupVersion.WithKind("Event"): {}, +} + func ValidateLabelName(labelName string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if !validation.IsQualifiedName(labelName) { @@ -145,6 +152,36 @@ func ValidateEndpointsSpecificAnnotations(annotations map[string]string, fldPath return allErrs } +func validateOwnerReference(ownerReference api.OwnerReference, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + gvk := unversioned.FromAPIVersionAndKind(ownerReference.APIVersion, ownerReference.Kind) + // gvk.Group is empty for the legacy group. + if len(gvk.Version) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVersion"), ownerReference.APIVersion, "version must not be empty")) + } + if len(gvk.Kind) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), ownerReference.Kind, "kind must not be empty")) + } + if len(ownerReference.Name) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), ownerReference.Name, "name must not be empty")) + } + if len(ownerReference.UID) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("uid"), ownerReference.UID, "uid must not be empty")) + } + if _, ok := BannedOwners[*gvk]; ok { + allErrs = append(allErrs, field.Invalid(fldPath, ownerReference, fmt.Sprintf("%s is disallowed from being an owner", gvk))) + } + return allErrs +} + +func ValidateOwnerReferences(ownerReferences []api.OwnerReference, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + for _, ref := range ownerReferences { + allErrs = append(allErrs, validateOwnerReference(ref, fldPath)...) + } + return allErrs +} + // ValidateNameFunc validates that the provided name is valid for a given resource type. // Not all resources have the same validation rules for names. Prefix is true if the // name will have a value appended to it. @@ -326,6 +363,7 @@ func ValidateObjectMeta(meta *api.ObjectMeta, requiresNamespace bool, nameFn Val allErrs = append(allErrs, ValidateNonnegativeField(meta.Generation, fldPath.Child("generation"))...) allErrs = append(allErrs, ValidateLabels(meta.Labels, fldPath.Child("labels"))...) allErrs = append(allErrs, ValidateAnnotations(meta.Annotations, fldPath.Child("annotations"))...) + allErrs = append(allErrs, ValidateOwnerReferences(meta.OwnerReferences, fldPath.Child("ownerReferences"))...) return allErrs } @@ -376,9 +414,11 @@ func ValidateObjectMetaUpdate(newMeta, oldMeta *api.ObjectMeta, fldPath *field.P allErrs = append(allErrs, ValidateImmutableField(newMeta.Namespace, oldMeta.Namespace, fldPath.Child("namespace"))...) allErrs = append(allErrs, ValidateImmutableField(newMeta.UID, oldMeta.UID, fldPath.Child("uid"))...) allErrs = append(allErrs, ValidateImmutableField(newMeta.CreationTimestamp, oldMeta.CreationTimestamp, fldPath.Child("creationTimestamp"))...) + allErrs = append(allErrs, ValidateImmutableField(newMeta.Finalizers, oldMeta.Finalizers, fldPath.Child("finalizers"))...) allErrs = append(allErrs, ValidateLabels(newMeta.Labels, fldPath.Child("labels"))...) allErrs = append(allErrs, ValidateAnnotations(newMeta.Annotations, fldPath.Child("annotations"))...) + allErrs = append(allErrs, ValidateOwnerReferences(newMeta.OwnerReferences, fldPath.Child("ownerReferences"))...) return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index a9811fdb9c0..60d35ec0b42 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -98,6 +98,56 @@ func TestValidateObjectMetaNamespaces(t *testing.T) { } } +func TestValidateObjectMetaOwnerReferences(t *testing.T) { + testCases := []struct { + ownerReferences []api.OwnerReference + expectError bool + }{ + { + []api.OwnerReference{ + { + APIVersion: "thirdpartyVersion", + Kind: "thirdpartyKind", + Name: "name", + UID: "1", + }, + }, + false, + }, + { + // event shouldn't be set as an owner + []api.OwnerReference{ + { + APIVersion: "v1", + Kind: "Event", + Name: "name", + UID: "1", + }, + }, + true, + }, + } + + for _, tc := range testCases { + errs := ValidateObjectMeta( + &api.ObjectMeta{Name: "test", Namespace: "test", OwnerReferences: tc.ownerReferences}, + true, + func(s string, prefix bool) (bool, string) { + return true, "" + }, + field.NewPath("field")) + if len(errs) != 0 && !tc.expectError { + t.Errorf("unexpected error: %v", errs) + } + if len(errs) == 0 && tc.expectError { + t.Errorf("expect error") + } + if len(errs) != 0 && !strings.Contains(errs[0].Error(), "is disallowed from being an owner") { + t.Errorf("unexpected error message: %v", errs) + } + } +} + func TestValidateObjectMetaUpdateIgnoresCreationTimestamp(t *testing.T) { if errs := ValidateObjectMetaUpdate( &api.ObjectMeta{Name: "test", ResourceVersion: "1"}, @@ -217,6 +267,21 @@ func TestValidateObjectMetaTrimsTrailingSlash(t *testing.T) { } } +// Ensure updating finalizers is disallowed +func TestValidateObjectMetaUpdateDisallowsUpdatingFinalizers(t *testing.T) { + errs := ValidateObjectMetaUpdate( + &api.ObjectMeta{Name: "test", ResourceVersion: "1", Finalizers: []string{"orphaning"}}, + &api.ObjectMeta{Name: "test", ResourceVersion: "1"}, + field.NewPath("field"), + ) + if len(errs) != 1 { + t.Fatalf("unexpected errors: %v", errs) + } + if !strings.Contains(errs[0].Error(), "field is immutable") { + t.Errorf("unexpected error message: %v", errs) + } +} + func TestValidateLabels(t *testing.T) { successCases := []map[string]string{ {"simple": "bar"},