Merge pull request #46101 from sttts/sttts-crd-core-names

Automatic merge from submit-queue

apiextensions: add Established condition

This introduces a `Established` condition on `CustomResourceDefinition`s. `Established` means that the resource has become active. A resource is established when all names are accepted initially without a conflict. A resource stays established until deleted, even during a later NameConflict due to changed names. Note that not all names can be changed.

This change is necessary to allow deletion of once-active CRDs which might have still instances, but  have NameConflicts now. Before this PR the REST endpoint was not active anymore in this case, making deletion of the instances impossible.
This commit is contained in:
Kubernetes Submit Queue 2017-05-24 02:13:32 -07:00 committed by GitHub
commit 7c76e3994c
10 changed files with 566 additions and 160 deletions

View File

@ -70,8 +70,13 @@ const (
type CustomResourceDefinitionConditionType string type CustomResourceDefinitionConditionType string
const ( const (
// NameConflict means the names chosen for this CustomResourceDefinition conflict with others in the group. // Established means that the resource has become active. A resource is established when all names are
NameConflict CustomResourceDefinitionConditionType = "NameConflict" // accepted without a conflict for the first time. A resource stays established until deleted, even during
// a later NamesAccepted due to changed names. Note that not all names can be changed.
Established CustomResourceDefinitionConditionType = "Established"
// NamesAccepted means the names chosen for this CustomResourceDefinition do not conflict with others in
// the group and are therefore accepted.
NamesAccepted CustomResourceDefinitionConditionType = "NamesAccepted"
// Terminating means that the CustomResourceDefinition has been deleted and is cleaning up. // Terminating means that the CustomResourceDefinition has been deleted and is cleaning up.
Terminating CustomResourceDefinitionConditionType = "Terminating" Terminating CustomResourceDefinitionConditionType = "Terminating"
) )

View File

@ -70,8 +70,13 @@ const (
type CustomResourceDefinitionConditionType string type CustomResourceDefinitionConditionType string
const ( const (
// NameConflict means the names chosen for this CustomResourceDefinition conflict with others in the group. // Established means that the resource has become active. A resource is established when all names are
NameConflict CustomResourceDefinitionConditionType = "NameConflict" // accepted without a conflict for the first time. A resource stays established until deleted, even during
// a later NamesAccepted due to changed names. Note that not all names can be changed.
Established CustomResourceDefinitionConditionType = "Established"
// NamesAccepted means the names chosen for this CustomResourceDefinition do not conflict with others in
// the group and are therefore accepted.
NamesAccepted CustomResourceDefinitionConditionType = "NamesAccepted"
// Terminating means that the CustomResourceDefinition has been deleted and is cleaning up. // Terminating means that the CustomResourceDefinition has been deleted and is cleaning up.
Terminating CustomResourceDefinitionConditionType = "Terminating" Terminating CustomResourceDefinitionConditionType = "Terminating"
) )

View File

@ -46,7 +46,7 @@ func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinitio
// ValidateCustomResourceDefinitionUpdate statically validates // ValidateCustomResourceDefinitionUpdate statically validates
func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList { func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList {
allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata")) allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateCustomResourceDefinitionSpecUpdate(&obj.Spec, &oldObj.Spec, field.NewPath("spec"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionSpecUpdate(&obj.Spec, &oldObj.Spec, apiextensions.IsCRDConditionTrue(oldObj, apiextensions.Established), field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
return allErrs return allErrs
} }
@ -64,22 +64,21 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
if len(spec.Group) == 0 { if len(spec.Group) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("group"), "")) allErrs = append(allErrs, field.Required(fldPath.Child("group"), ""))
} } else if errs := validationutil.IsDNS1123Subdomain(spec.Group); len(errs) > 0 {
if errs := validationutil.IsDNS1123Subdomain(spec.Group); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, strings.Join(errs, ","))) allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, strings.Join(errs, ",")))
} } else if len(strings.Split(spec.Group, ".")) < 2 {
if len(strings.Split(spec.Group, ".")) < 2 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, "should be a domain with at least one dot")) allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, "should be a domain with at least one dot"))
} }
if len(spec.Version) == 0 { if len(spec.Version) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("version"), "")) allErrs = append(allErrs, field.Required(fldPath.Child("version"), ""))
} } else if errs := validationutil.IsDNS1035Label(spec.Version); len(errs) > 0 {
if errs := validationutil.IsDNS1035Label(spec.Version); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ","))) allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ",")))
} }
switch spec.Scope { switch spec.Scope {
case "":
allErrs = append(allErrs, field.Required(fldPath.Child("scope"), ""))
case apiextensions.ClusterScoped, apiextensions.NamespaceScoped: case apiextensions.ClusterScoped, apiextensions.NamespaceScoped:
default: default:
allErrs = append(allErrs, field.NotSupported(fldPath.Child("scope"), spec.Scope, []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)})) allErrs = append(allErrs, field.NotSupported(fldPath.Child("scope"), spec.Scope, []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)}))
@ -105,17 +104,19 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
} }
// ValidateCustomResourceDefinitionSpecUpdate statically validates // ValidateCustomResourceDefinitionSpecUpdate statically validates
func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList { func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, established bool, fldPath *field.Path) field.ErrorList {
allErrs := ValidateCustomResourceDefinitionSpec(spec, fldPath) allErrs := ValidateCustomResourceDefinitionSpec(spec, fldPath)
// these all affect the storage, so you can't change them if established {
genericvalidation.ValidateImmutableField(spec.Group, oldSpec.Group, fldPath.Child("group")) // these effect the storage and cannot be changed therefore
genericvalidation.ValidateImmutableField(spec.Version, oldSpec.Version, fldPath.Child("version")) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Version, oldSpec.Version, fldPath.Child("version"))...)
genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope")) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope"))...)
genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind")) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind"))...)
}
// this affects the expected resource name, so you can't change it either // these affects the resource name, which is always immutable, so this can't be updated.
genericvalidation.ValidateImmutableField(spec.Names.Plural, oldSpec.Names.Plural, fldPath.Child("names", "plural")) allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Group, oldSpec.Group, fldPath.Child("group"))...)
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Plural, oldSpec.Names.Plural, fldPath.Child("names", "plural"))...)
return allErrs return allErrs
} }

View File

@ -29,11 +29,17 @@ type validationMatch struct {
errorType field.ErrorType errorType field.ErrorType
} }
func required(path *field.Path) validationMatch { func required(path ...string) validationMatch {
return validationMatch{path: path, errorType: field.ErrorTypeRequired} return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeRequired}
} }
func invalid(path *field.Path) validationMatch { func invalid(path ...string) validationMatch {
return validationMatch{path: path, errorType: field.ErrorTypeInvalid} return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
}
func unsupported(path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeNotSupported}
}
func immutable(path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
} }
func (v validationMatch) matches(err *field.Error) bool { func (v validationMatch) matches(err *field.Error) bool {
@ -58,7 +64,12 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
}, },
}, },
errors: []validationMatch{ errors: []validationMatch{
invalid(field.NewPath("metadata", "name")), invalid("metadata", "name"),
required("spec", "version"),
required("spec", "scope"),
required("spec", "names", "singular"),
required("spec", "names", "kind"),
required("spec", "names", "listKind"),
}, },
}, },
{ {
@ -67,13 +78,14 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
}, },
errors: []validationMatch{ errors: []validationMatch{
required(field.NewPath("spec", "group")), invalid("metadata", "name"),
required(field.NewPath("spec", "version")), required("spec", "group"),
{path: field.NewPath("spec", "scope"), errorType: field.ErrorTypeNotSupported}, required("spec", "version"),
required(field.NewPath("spec", "names", "plural")), required("spec", "scope"),
required(field.NewPath("spec", "names", "singular")), required("spec", "names", "plural"),
required(field.NewPath("spec", "names", "kind")), required("spec", "names", "singular"),
required(field.NewPath("spec", "names", "listKind")), required("spec", "names", "kind"),
required("spec", "names", "listKind"),
}, },
}, },
{ {
@ -101,17 +113,20 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
}, },
}, },
errors: []validationMatch{ errors: []validationMatch{
invalid(field.NewPath("spec", "group")), invalid("metadata", "name"),
invalid(field.NewPath("spec", "version")), invalid("spec", "group"),
{path: field.NewPath("spec", "scope"), errorType: field.ErrorTypeNotSupported}, invalid("spec", "version"),
invalid(field.NewPath("spec", "names", "plural")), unsupported("spec", "scope"),
invalid(field.NewPath("spec", "names", "singular")), invalid("spec", "names", "plural"),
invalid(field.NewPath("spec", "names", "kind")), invalid("spec", "names", "singular"),
invalid(field.NewPath("spec", "names", "listKind")), invalid("spec", "names", "kind"),
invalid(field.NewPath("status", "acceptedNames", "plural")), invalid("spec", "names", "listKind"), // invalid format
invalid(field.NewPath("status", "acceptedNames", "singular")), invalid("spec", "names", "listKind"), // kind == listKind
invalid(field.NewPath("status", "acceptedNames", "kind")), invalid("status", "acceptedNames", "plural"),
invalid(field.NewPath("status", "acceptedNames", "listKind")), invalid("status", "acceptedNames", "singular"),
invalid("status", "acceptedNames", "kind"),
invalid("status", "acceptedNames", "listKind"), // invalid format
invalid("status", "acceptedNames", "listKind"), // kind == listKind
}, },
}, },
{ {
@ -138,21 +153,25 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
}, },
}, },
errors: []validationMatch{ errors: []validationMatch{
invalid(field.NewPath("spec", "group")), invalid("metadata", "name"),
invalid(field.NewPath("spec", "names", "listKind")), invalid("spec", "group"),
invalid(field.NewPath("status", "acceptedNames", "listKind")), required("spec", "scope"),
invalid("spec", "names", "listKind"),
invalid("status", "acceptedNames", "listKind"),
}, },
}, },
} }
for _, tc := range tests { for _, tc := range tests {
errs := ValidateCustomResourceDefinition(tc.resource) errs := ValidateCustomResourceDefinition(tc.resource)
seenErrs := make([]bool, len(errs))
for _, expectedError := range tc.errors { for _, expectedError := range tc.errors {
found := false found := false
for _, err := range errs { for i, err := range errs {
if expectedError.matches(err) { if expectedError.matches(err) && !seenErrs[i] {
found = true found = true
seenErrs[i] = true
break break
} }
} }
@ -161,5 +180,281 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
t.Errorf("%s: expected %v at %v, got %v", tc.name, expectedError.errorType, expectedError.path.String(), errs) t.Errorf("%s: expected %v at %v, got %v", tc.name, expectedError.errorType, expectedError.path.String(), errs)
} }
} }
for i, seen := range seenErrs {
if !seen {
t.Errorf("%s: unexpected error: %v", tc.name, errs[i])
}
}
}
}
func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
tests := []struct {
name string
old *apiextensions.CustomResourceDefinition
resource *apiextensions.CustomResourceDefinition
errors []validationMatch
}{
{
name: "unchanged",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Scope: apiextensions.ResourceScope("Cluster"),
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
AcceptedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Scope: apiextensions.ResourceScope("Cluster"),
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
AcceptedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
},
errors: []validationMatch{},
},
{
name: "unchanged-established",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Scope: apiextensions.ResourceScope("Cluster"),
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
AcceptedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
Conditions: []apiextensions.CustomResourceDefinitionCondition{
{Type: apiextensions.Established, Status: apiextensions.ConditionTrue},
},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Scope: apiextensions.ResourceScope("Cluster"),
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
AcceptedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
},
errors: []validationMatch{},
},
{
name: "changes",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Scope: apiextensions.ResourceScope("Cluster"),
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
AcceptedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
Conditions: []apiextensions.CustomResourceDefinitionCondition{
{Type: apiextensions.Established, Status: apiextensions.ConditionFalse},
},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "abc.com",
Version: "version2",
Scope: apiextensions.ResourceScope("Namespaced"),
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural2",
Singular: "singular2",
Kind: "kind2",
ListKind: "listkind2",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
AcceptedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "plural2",
Singular: "singular2",
Kind: "kind2",
ListKind: "listkind2",
},
},
},
errors: []validationMatch{
immutable("spec", "group"),
immutable("spec", "names", "plural"),
},
},
{
name: "changes-established",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Scope: apiextensions.ResourceScope("Cluster"),
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
AcceptedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
Conditions: []apiextensions.CustomResourceDefinitionCondition{
{Type: apiextensions.Established, Status: apiextensions.ConditionTrue},
},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "abc.com",
Version: "version2",
Scope: apiextensions.ResourceScope("Namespaced"),
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural2",
Singular: "singular2",
Kind: "kind2",
ListKind: "listkind2",
},
},
Status: apiextensions.CustomResourceDefinitionStatus{
AcceptedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "plural2",
Singular: "singular2",
Kind: "kind2",
ListKind: "listkind2",
},
},
},
errors: []validationMatch{
immutable("spec", "group"),
immutable("spec", "version"),
immutable("spec", "scope"),
immutable("spec", "names", "kind"),
immutable("spec", "names", "plural"),
},
},
}
for _, tc := range tests {
errs := ValidateCustomResourceDefinitionUpdate(tc.resource, tc.old)
seenErrs := make([]bool, len(errs))
for _, expectedError := range tc.errors {
found := false
for i, err := range errs {
if expectedError.matches(err) && !seenErrs[i] {
found = true
seenErrs[i] = true
break
}
}
if !found {
t.Errorf("%s: expected %v at %v, got %v", tc.name, expectedError.errorType, expectedError.path.String(), errs)
}
}
for i, seen := range seenErrs {
if !seen {
t.Errorf("%s: unexpected error: %v", tc.name, errs[i])
}
}
} }
} }

View File

@ -85,8 +85,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
foundVersion := false foundVersion := false
foundGroup := false foundGroup := false
for _, crd := range crds { for _, crd := range crds {
// if we can't definitively determine that our names are good, don't serve it if !apiextensions.IsCRDConditionTrue(crd, apiextensions.Established) {
if !apiextensions.IsCRDConditionFalse(crd, apiextensions.NameConflict) {
continue continue
} }

View File

@ -143,8 +143,7 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.delegate.ServeHTTP(w, req) r.delegate.ServeHTTP(w, req)
return return
} }
// if we can't definitively determine that our names are good, delegate if !apiextensions.IsCRDConditionTrue(crd, apiextensions.Established) {
if !apiextensions.IsCRDConditionFalse(crd, apiextensions.NameConflict) {
r.delegate.ServeHTTP(w, req) r.delegate.ServeHTTP(w, req)
} }
if len(requestInfo.Subresource) > 0 { if len(requestInfo.Subresource) > 0 {

View File

@ -127,38 +127,55 @@ func (c *CRDFinalizer) sync(key string) error {
return err return err
} }
// It's possible for a naming conflict to have removed this resource from the API after instances were created. // Now we can start deleting items. We should use the REST API to ensure that all normal admission runs.
// For now we will cowardly stop finalizing. If we don't go through the REST API, weird things may happen: // Since we control the endpoints, we know that delete collection works. No need to delete if not established.
// no audit trail, no admission checks or side effects, finalization would probably still work but defaulting if apiextensions.IsCRDConditionTrue(crd, apiextensions.Established) {
// would be missed. It would be a mess. cond, deleteErr := c.deleteInstances(crd)
// This requires human intervention to solve, update status so they have a reason. apiextensions.SetCRDCondition(crd, *cond)
// TODO split coreNamesAccepted from extendedNamesAccepted. If coreNames were accepted, then we have something to cleanup if deleteErr != nil {
// and the endpoint is serviceable. if they aren't, then there's nothing to cleanup. crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if !apiextensions.IsCRDConditionFalse(crd, apiextensions.NameConflict) { if err != nil {
utilruntime.HandleError(err)
}
return deleteErr
}
} else {
apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{ apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating, Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue, Status: apiextensions.ConditionFalse,
Reason: "InstanceDeletionStuck", Reason: "NeverEstablished",
Message: fmt.Sprintf("cannot proceed with deletion because of %v condition", apiextensions.NameConflict), Message: "resource was never established",
}) })
}
apiextensions.CRDRemoveFinalizer(crd, apiextensions.CustomResourceCleanupFinalizer)
crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil { if err != nil {
return err return err
} }
return fmt.Errorf("cannot proceed with deletion because of %v condition", apiextensions.NameConflict)
}
// and now issue another delete, which should clean it all up if no finalizers remain or no-op if they do
return c.crdClient.CustomResourceDefinitions().Delete(crd.Name, nil)
}
func (c *CRDFinalizer) deleteInstances(crd *apiextensions.CustomResourceDefinition) (*apiextensions.CustomResourceDefinitionCondition, error) {
// Now we can start deleting items. While it would be ideal to use a REST API client, doing so // Now we can start deleting items. While it would be ideal to use a REST API client, doing so
// could incorrectly delete a ThirdPartyResource with the same URL as the CustomResource, so we go // could incorrectly delete a ThirdPartyResource with the same URL as the CustomResource, so we go
// directly to the storage instead. Since we control the storage, we know that delete collection works. // directly to the storage instead. Since we control the storage, we know that delete collection works.
crClient := c.crClientGetter.GetCustomResourceListerCollectionDeleter(crd.UID) crClient := c.crClientGetter.GetCustomResourceListerCollectionDeleter(crd.UID)
if crClient == nil { if crClient == nil {
return fmt.Errorf("unable to find a custom resource client for %s.%s", crd.Status.AcceptedNames.Plural, crd.Spec.Group) return nil, fmt.Errorf("unable to find a custom resource client for %s.%s", crd.Status.AcceptedNames.Plural, crd.Spec.Group)
} }
ctx := genericapirequest.NewContext() ctx := genericapirequest.NewContext()
allResources, err := crClient.List(ctx, nil) allResources, err := crClient.List(ctx, nil)
if err != nil { if err != nil {
return err return &apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionFailed",
Message: fmt.Sprintf("could not list instances: %v", err),
}, err
} }
deletedNamespaces := sets.String{} deletedNamespaces := sets.String{}
@ -181,23 +198,18 @@ func (c *CRDFinalizer) sync(key string) error {
} }
} }
if deleteError := utilerrors.NewAggregate(deleteErrors); deleteError != nil { if deleteError := utilerrors.NewAggregate(deleteErrors); deleteError != nil {
apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{ return &apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating, Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue, Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionFailed", Reason: "InstanceDeletionFailed",
Message: fmt.Sprintf("could not issue all deletes: %v", deleteError), Message: fmt.Sprintf("could not issue all deletes: %v", deleteError),
}) }, deleteError
crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil {
utilruntime.HandleError(err)
}
return deleteError
} }
// now we need to wait until all the resources are deleted. Start with a simple poll before we do anything fancy. // now we need to wait until all the resources are deleted. Start with a simple poll before we do anything fancy.
// TODO not all servers are synchronized on caches. It is possible for a stale one to still be creating things. // TODO not all servers are synchronized on caches. It is possible for a stale one to still be creating things.
// Once we have a mechanism for servers to indicate their states, we should check that for concurrence. // Once we have a mechanism for servers to indicate their states, we should check that for concurrence.
listErr := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) { err = wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) {
listObj, err := crClient.List(ctx, nil) listObj, err := crClient.List(ctx, nil)
if err != nil { if err != nil {
return false, err return false, err
@ -208,34 +220,20 @@ func (c *CRDFinalizer) sync(key string) error {
glog.V(2).Infof("%s.%s waiting for %d items to be removed", crd.Status.AcceptedNames.Plural, crd.Spec.Group, len(listObj.(*unstructured.UnstructuredList).Items)) glog.V(2).Infof("%s.%s waiting for %d items to be removed", crd.Status.AcceptedNames.Plural, crd.Spec.Group, len(listObj.(*unstructured.UnstructuredList).Items))
return false, nil return false, nil
}) })
if listErr != nil { if err != nil {
apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{ return &apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating, Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue, Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionCheck", Reason: "InstanceDeletionCheck",
Message: fmt.Sprintf("could not confirm zero CustomResources remaining: %v", listErr), Message: fmt.Sprintf("could not confirm zero CustomResources remaining: %v", err),
}) }, err
crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil {
utilruntime.HandleError(err)
} }
return listErr return &apiextensions.CustomResourceDefinitionCondition{
}
apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating, Type: apiextensions.Terminating,
Status: apiextensions.ConditionFalse, Status: apiextensions.ConditionFalse,
Reason: "InstanceDeletionCompleted", Reason: "InstanceDeletionCompleted",
Message: "removed all instances", Message: "removed all instances",
}) }, nil
apiextensions.CRDRemoveFinalizer(crd, apiextensions.CustomResourceCleanupFinalizer)
crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil {
return err
}
// and now issue another delete, which should clean it all up if no finalizers remain or no-op if they do
return c.crdClient.CustomResourceDefinitions().Delete(crd.Name, nil)
} }
func (c *CRDFinalizer) Run(workers int, stopCh <-chan struct{}) { func (c *CRDFinalizer) Run(workers int, stopCh <-chan struct{}) {

View File

@ -28,6 +28,7 @@ go_library(
deps = [ deps = [
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",

View File

@ -24,6 +24,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
utilerrors "k8s.io/apimachinery/pkg/util/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors"
@ -118,12 +119,12 @@ func (c *NamingConditionController) getAcceptedNamesForGroup(group string) (allR
return allResources, allKinds return allResources, allKinds
} }
func (c *NamingConditionController) calculateNames(in *apiextensions.CustomResourceDefinition) (apiextensions.CustomResourceDefinitionNames, apiextensions.CustomResourceDefinitionCondition) { func (c *NamingConditionController) calculateNamesAndConditions(in *apiextensions.CustomResourceDefinition) (apiextensions.CustomResourceDefinitionNames, apiextensions.CustomResourceDefinitionCondition, apiextensions.CustomResourceDefinitionCondition) {
// Get the names that have already been claimed // Get the names that have already been claimed
allResources, allKinds := c.getAcceptedNamesForGroup(in.Spec.Group) allResources, allKinds := c.getAcceptedNamesForGroup(in.Spec.Group)
condition := apiextensions.CustomResourceDefinitionCondition{ namesAcceptedCondition := apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.NameConflict, Type: apiextensions.NamesAccepted,
Status: apiextensions.ConditionUnknown, Status: apiextensions.ConditionUnknown,
} }
@ -134,16 +135,16 @@ func (c *NamingConditionController) calculateNames(in *apiextensions.CustomResou
// Check each name for mismatches. If there's a mismatch between spec and status, then try to deconflict. // Check each name for mismatches. If there's a mismatch between spec and status, then try to deconflict.
// Continue on errors so that the status is the best match possible // Continue on errors so that the status is the best match possible
if err := equalToAcceptedOrFresh(requestedNames.Plural, acceptedNames.Plural, allResources); err != nil { if err := equalToAcceptedOrFresh(requestedNames.Plural, acceptedNames.Plural, allResources); err != nil {
condition.Status = apiextensions.ConditionTrue namesAcceptedCondition.Status = apiextensions.ConditionFalse
condition.Reason = "Plural" namesAcceptedCondition.Reason = "PluralConflict"
condition.Message = err.Error() namesAcceptedCondition.Message = err.Error()
} else { } else {
newNames.Plural = requestedNames.Plural newNames.Plural = requestedNames.Plural
} }
if err := equalToAcceptedOrFresh(requestedNames.Singular, acceptedNames.Singular, allResources); err != nil { if err := equalToAcceptedOrFresh(requestedNames.Singular, acceptedNames.Singular, allResources); err != nil {
condition.Status = apiextensions.ConditionTrue namesAcceptedCondition.Status = apiextensions.ConditionFalse
condition.Reason = "Singular" namesAcceptedCondition.Reason = "SingularConflict"
condition.Message = err.Error() namesAcceptedCondition.Message = err.Error()
} else { } else {
newNames.Singular = requestedNames.Singular newNames.Singular = requestedNames.Singular
} }
@ -161,37 +162,58 @@ func (c *NamingConditionController) calculateNames(in *apiextensions.CustomResou
} }
if err := utilerrors.NewAggregate(errs); err != nil { if err := utilerrors.NewAggregate(errs); err != nil {
condition.Status = apiextensions.ConditionTrue namesAcceptedCondition.Status = apiextensions.ConditionFalse
condition.Reason = "ShortNames" namesAcceptedCondition.Reason = "ShortNamesConflict"
condition.Message = err.Error() namesAcceptedCondition.Message = err.Error()
} else { } else {
newNames.ShortNames = requestedNames.ShortNames newNames.ShortNames = requestedNames.ShortNames
} }
} }
if err := equalToAcceptedOrFresh(requestedNames.Kind, acceptedNames.Kind, allKinds); err != nil { if err := equalToAcceptedOrFresh(requestedNames.Kind, acceptedNames.Kind, allKinds); err != nil {
condition.Status = apiextensions.ConditionTrue namesAcceptedCondition.Status = apiextensions.ConditionFalse
condition.Reason = "Kind" namesAcceptedCondition.Reason = "KindConflict"
condition.Message = err.Error() namesAcceptedCondition.Message = err.Error()
} else { } else {
newNames.Kind = requestedNames.Kind newNames.Kind = requestedNames.Kind
} }
if err := equalToAcceptedOrFresh(requestedNames.ListKind, acceptedNames.ListKind, allKinds); err != nil { if err := equalToAcceptedOrFresh(requestedNames.ListKind, acceptedNames.ListKind, allKinds); err != nil {
condition.Status = apiextensions.ConditionTrue namesAcceptedCondition.Status = apiextensions.ConditionFalse
condition.Reason = "ListKind" namesAcceptedCondition.Reason = "ListKindConflict"
condition.Message = err.Error() namesAcceptedCondition.Message = err.Error()
} else { } else {
newNames.ListKind = requestedNames.ListKind newNames.ListKind = requestedNames.ListKind
} }
// if we haven't changed the condition, then our names must be good. // if we haven't changed the condition, then our names must be good.
if condition.Status == apiextensions.ConditionUnknown { if namesAcceptedCondition.Status == apiextensions.ConditionUnknown {
condition.Status = apiextensions.ConditionFalse namesAcceptedCondition.Status = apiextensions.ConditionTrue
condition.Reason = "NoConflicts" namesAcceptedCondition.Reason = "NoConflicts"
condition.Message = "no conflicts found" namesAcceptedCondition.Message = "no conflicts found"
} }
return newNames, condition // set EstablishedCondition to true if all names are accepted. Never set it back to false.
establishedCondition := apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionFalse,
Reason: "NotAccepted",
Message: "not all names are accepted",
LastTransitionTime: metav1.NewTime(time.Now()),
}
if old := apiextensions.FindCRDCondition(in, apiextensions.Established); old != nil {
establishedCondition = *old
}
if establishedCondition.Status != apiextensions.ConditionTrue && namesAcceptedCondition.Status == apiextensions.ConditionTrue {
establishedCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionTrue,
Reason: "InitialNamesAccepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.NewTime(time.Now()),
}
}
return newNames, namesAcceptedCondition, establishedCondition
} }
func equalToAcceptedOrFresh(requestedName, acceptedName string, usedNames sets.String) error { func equalToAcceptedOrFresh(requestedName, acceptedName string, usedNames sets.String) error {
@ -214,12 +236,12 @@ func (c *NamingConditionController) sync(key string) error {
return err return err
} }
acceptedNames, namingCondition := c.calculateNames(inCustomResourceDefinition) acceptedNames, namingCondition, establishedCondition := c.calculateNamesAndConditions(inCustomResourceDefinition)
// nothing to do if accepted names and NameConflict condition didn't change
// nothing to do if accepted names and NamesAccepted condition didn't change
if reflect.DeepEqual(inCustomResourceDefinition.Status.AcceptedNames, acceptedNames) && if reflect.DeepEqual(inCustomResourceDefinition.Status.AcceptedNames, acceptedNames) &&
apiextensions.IsCRDConditionEquivalent( apiextensions.IsCRDConditionEquivalent(&namingCondition, apiextensions.FindCRDCondition(inCustomResourceDefinition, apiextensions.NamesAccepted)) &&
&namingCondition, apiextensions.IsCRDConditionEquivalent(&establishedCondition, apiextensions.FindCRDCondition(inCustomResourceDefinition, apiextensions.Established)) {
apiextensions.FindCRDCondition(inCustomResourceDefinition, apiextensions.NameConflict)) {
return nil return nil
} }
@ -230,6 +252,7 @@ func (c *NamingConditionController) sync(key string) error {
crd.Status.AcceptedNames = acceptedNames crd.Status.AcceptedNames = acceptedNames
apiextensions.SetCRDCondition(crd, namingCondition) apiextensions.SetCRDCondition(crd, namingCondition)
apiextensions.SetCRDCondition(crd, establishedCondition)
updatedObj, err := c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) updatedObj, err := c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil { if err != nil {

View File

@ -67,6 +67,12 @@ func (b *crdBuilder) StatusNames(plural, singular, kind, listKind string, shortN
return b return b
} }
func (b *crdBuilder) Condition(c apiextensions.CustomResourceDefinitionCondition) *crdBuilder {
b.curr.Status.Conditions = append(b.curr.Status.Conditions, c)
return b
}
func names(plural, singular, kind, listKind string, shortNames ...string) apiextensions.CustomResourceDefinitionNames { func names(plural, singular, kind, listKind string, shortNames ...string) apiextensions.CustomResourceDefinitionNames {
ret := apiextensions.CustomResourceDefinitionNames{ ret := apiextensions.CustomResourceDefinitionNames{
Plural: plural, Plural: plural,
@ -82,22 +88,36 @@ func (b *crdBuilder) NewOrDie() *apiextensions.CustomResourceDefinition {
return &b.curr return &b.curr
} }
var goodCondition = apiextensions.CustomResourceDefinitionCondition{ var acceptedCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.NameConflict, Type: apiextensions.NamesAccepted,
Status: apiextensions.ConditionFalse, Status: apiextensions.ConditionTrue,
Reason: "NoConflicts", Reason: "NoConflicts",
Message: "no conflicts found", Message: "no conflicts found",
} }
func badCondition(reason, message string) apiextensions.CustomResourceDefinitionCondition { func nameConflictCondition(reason, message string) apiextensions.CustomResourceDefinitionCondition {
return apiextensions.CustomResourceDefinitionCondition{ return apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.NameConflict, Type: apiextensions.NamesAccepted,
Status: apiextensions.ConditionTrue, Status: apiextensions.ConditionFalse,
Reason: reason, Reason: reason,
Message: message, Message: message,
} }
} }
var establishedCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionTrue,
Reason: "InitialNamesAccepted",
Message: "the initial names have been accepted",
}
var notEstablishedCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionFalse,
Reason: "NotAccepted",
Message: "not all names are accepted",
}
func TestSync(t *testing.T) { func TestSync(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -105,7 +125,8 @@ func TestSync(t *testing.T) {
in *apiextensions.CustomResourceDefinition in *apiextensions.CustomResourceDefinition
existing []*apiextensions.CustomResourceDefinition existing []*apiextensions.CustomResourceDefinition
expectedNames apiextensions.CustomResourceDefinitionNames expectedNames apiextensions.CustomResourceDefinitionNames
expectedCondition apiextensions.CustomResourceDefinitionCondition expectedNameConflictCondition apiextensions.CustomResourceDefinitionCondition
expectedEstablishedCondition apiextensions.CustomResourceDefinitionCondition
}{ }{
{ {
name: "first resource", name: "first resource",
@ -114,7 +135,8 @@ func TestSync(t *testing.T) {
expectedNames: apiextensions.CustomResourceDefinitionNames{ expectedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "alfa", Plural: "alfa",
}, },
expectedCondition: goodCondition, expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: establishedCondition,
}, },
{ {
name: "different groups", name: "different groups",
@ -123,7 +145,8 @@ func TestSync(t *testing.T) {
newCRD("alfa.charlie.com").StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(), newCRD("alfa.charlie.com").StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
}, },
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedCondition: goodCondition, expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: establishedCondition,
}, },
{ {
name: "conflict plural to singular", name: "conflict plural to singular",
@ -132,7 +155,8 @@ func TestSync(t *testing.T) {
newCRD("india.bravo.com").StatusNames("india", "alfa", "", "").NewOrDie(), newCRD("india.bravo.com").StatusNames("india", "alfa", "", "").NewOrDie(),
}, },
expectedNames: names("", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), expectedNames: names("", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedCondition: badCondition("Plural", `"alfa" is already in use`), expectedNameConflictCondition: nameConflictCondition("PluralConflict", `"alfa" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
}, },
{ {
name: "conflict singular to shortName", name: "conflict singular to shortName",
@ -141,7 +165,8 @@ func TestSync(t *testing.T) {
newCRD("india.bravo.com").StatusNames("india", "indias", "", "", "delta-singular").NewOrDie(), newCRD("india.bravo.com").StatusNames("india", "indias", "", "", "delta-singular").NewOrDie(),
}, },
expectedNames: names("alfa", "", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), expectedNames: names("alfa", "", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedCondition: badCondition("Singular", `"delta-singular" is already in use`), expectedNameConflictCondition: nameConflictCondition("SingularConflict", `"delta-singular" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
}, },
{ {
name: "conflict on shortName to shortName", name: "conflict on shortName to shortName",
@ -150,7 +175,8 @@ func TestSync(t *testing.T) {
newCRD("india.bravo.com").StatusNames("india", "indias", "", "", "hotel-shortname-2").NewOrDie(), newCRD("india.bravo.com").StatusNames("india", "indias", "", "", "hotel-shortname-2").NewOrDie(),
}, },
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind"), expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind"),
expectedCondition: badCondition("ShortNames", `"hotel-shortname-2" is already in use`), expectedNameConflictCondition: nameConflictCondition("ShortNamesConflict", `"hotel-shortname-2" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
}, },
{ {
name: "conflict on kind to listkind", name: "conflict on kind to listkind",
@ -159,7 +185,8 @@ func TestSync(t *testing.T) {
newCRD("india.bravo.com").StatusNames("india", "indias", "", "echo-kind").NewOrDie(), newCRD("india.bravo.com").StatusNames("india", "indias", "", "echo-kind").NewOrDie(),
}, },
expectedNames: names("alfa", "delta-singular", "", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), expectedNames: names("alfa", "delta-singular", "", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedCondition: badCondition("Kind", `"echo-kind" is already in use`), expectedNameConflictCondition: nameConflictCondition("KindConflict", `"echo-kind" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
}, },
{ {
name: "conflict on listkind to kind", name: "conflict on listkind to kind",
@ -168,7 +195,8 @@ func TestSync(t *testing.T) {
newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "").NewOrDie(), newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "").NewOrDie(),
}, },
expectedNames: names("alfa", "delta-singular", "echo-kind", "", "golf-shortname-1", "hotel-shortname-2"), expectedNames: names("alfa", "delta-singular", "echo-kind", "", "golf-shortname-1", "hotel-shortname-2"),
expectedCondition: badCondition("ListKind", `"foxtrot-listkind" is already in use`), expectedNameConflictCondition: nameConflictCondition("ListKindConflict", `"foxtrot-listkind" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
}, },
{ {
name: "no conflict on resource and kind", name: "no conflict on resource and kind",
@ -177,7 +205,8 @@ func TestSync(t *testing.T) {
newCRD("india.bravo.com").StatusNames("india", "echo-kind", "", "").NewOrDie(), newCRD("india.bravo.com").StatusNames("india", "echo-kind", "", "").NewOrDie(),
}, },
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedCondition: goodCondition, expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: establishedCondition,
}, },
{ {
name: "merge on conflicts", name: "merge on conflicts",
@ -189,7 +218,8 @@ func TestSync(t *testing.T) {
newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "", "delta-singular").NewOrDie(), newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "", "delta-singular").NewOrDie(),
}, },
expectedNames: names("alfa", "yankee-singular", "echo-kind", "whiskey-listkind", "golf-shortname-1", "hotel-shortname-2"), expectedNames: names("alfa", "yankee-singular", "echo-kind", "whiskey-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedCondition: badCondition("ListKind", `"foxtrot-listkind" is already in use`), expectedNameConflictCondition: nameConflictCondition("ListKindConflict", `"foxtrot-listkind" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
}, },
{ {
name: "merge on conflicts shortNames as one", name: "merge on conflicts shortNames as one",
@ -201,7 +231,8 @@ func TestSync(t *testing.T) {
newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "", "delta-singular", "golf-shortname-1").NewOrDie(), newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "", "delta-singular", "golf-shortname-1").NewOrDie(),
}, },
expectedNames: names("alfa", "yankee-singular", "echo-kind", "whiskey-listkind", "victor-shortname-1", "uniform-shortname-2"), expectedNames: names("alfa", "yankee-singular", "echo-kind", "whiskey-listkind", "victor-shortname-1", "uniform-shortname-2"),
expectedCondition: badCondition("ListKind", `"foxtrot-listkind" is already in use`), expectedNameConflictCondition: nameConflictCondition("ListKindConflict", `"foxtrot-listkind" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
}, },
{ {
name: "no conflicts on self", name: "no conflicts on self",
@ -216,7 +247,8 @@ func TestSync(t *testing.T) {
NewOrDie(), NewOrDie(),
}, },
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"), expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedCondition: goodCondition, expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: establishedCondition,
}, },
{ {
name: "no conflicts on self, remove shortname", name: "no conflicts on self, remove shortname",
@ -231,7 +263,52 @@ func TestSync(t *testing.T) {
NewOrDie(), NewOrDie(),
}, },
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1"), expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1"),
expectedCondition: goodCondition, expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: establishedCondition,
},
{
name: "established before with true condition",
in: newCRD("alfa.bravo.com").Condition(establishedCondition).NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{},
expectedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "alfa",
},
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: establishedCondition,
},
{
name: "not established before with false condition",
in: newCRD("alfa.bravo.com").Condition(notEstablishedCondition).NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{},
expectedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "alfa",
},
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: establishedCondition,
},
{
name: "conflicting, established before with true condition",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
Condition(establishedCondition).
NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "alfa", "", "").NewOrDie(),
},
expectedNames: names("", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("PluralConflict", `"alfa" is already in use`),
expectedEstablishedCondition: establishedCondition,
},
{
name: "conflicting, not established before with false condition",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
Condition(notEstablishedCondition).
NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "alfa", "", "").NewOrDie(),
},
expectedNames: names("", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("PluralConflict", `"alfa" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
}, },
} }
@ -245,12 +322,15 @@ func TestSync(t *testing.T) {
crdLister: listers.NewCustomResourceDefinitionLister(crdIndexer), crdLister: listers.NewCustomResourceDefinitionLister(crdIndexer),
crdMutationCache: cache.NewIntegerResourceVersionMutationCache(crdIndexer, crdIndexer, 60*time.Second, false), crdMutationCache: cache.NewIntegerResourceVersionMutationCache(crdIndexer, crdIndexer, 60*time.Second, false),
} }
actualNames, actualCondition := c.calculateNames(tc.in) actualNames, actualNameConflictCondition, actualEstablishedCondition := c.calculateNamesAndConditions(tc.in)
if e, a := tc.expectedNames, actualNames; !reflect.DeepEqual(e, a) { if e, a := tc.expectedNames, actualNames; !reflect.DeepEqual(e, a) {
t.Errorf("%v expected %v, got %#v", tc.name, e, a) t.Errorf("%v expected %v, got %#v", tc.name, e, a)
} }
if e, a := tc.expectedCondition, actualCondition; !apiextensions.IsCRDConditionEquivalent(&e, &a) { if e, a := tc.expectedNameConflictCondition, actualNameConflictCondition; !apiextensions.IsCRDConditionEquivalent(&e, &a) {
t.Errorf("%v expected %v, got %v", tc.name, e, a)
}
if e, a := tc.expectedEstablishedCondition, actualEstablishedCondition; !apiextensions.IsCRDConditionEquivalent(&e, &a) {
t.Errorf("%v expected %v, got %v", tc.name, e, a) t.Errorf("%v expected %v, got %v", tc.name, e, a)
} }
} }