From 7e4bb365e6e7faf5520cf154a922786a1ea4e0dd Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Wed, 24 Mar 2021 12:16:55 +0100 Subject: [PATCH 1/6] Allows ordering of Custom Resources Signed-off-by: Niklas Wagner --- pkg/releaseutil/kind_sorter.go | 46 +++++++++++++++++++++--------- pkg/releaseutil/manifest.go | 3 ++ pkg/releaseutil/manifest_sorter.go | 43 ++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/pkg/releaseutil/kind_sorter.go b/pkg/releaseutil/kind_sorter.go index a340dfc29..89289c5de 100644 --- a/pkg/releaseutil/kind_sorter.go +++ b/pkg/releaseutil/kind_sorter.go @@ -108,12 +108,12 @@ var UninstallOrder KindSortOrder = []string{ // sort manifests by kind. // // Results are sorted by 'ordering', keeping order of items with equal kind/priority -func sortManifestsByKind(manifests []Manifest, ordering KindSortOrder) []Manifest { - sort.SliceStable(manifests, func(i, j int) bool { - return lessByKind(manifests[i], manifests[j], manifests[i].Head.Kind, manifests[j].Head.Kind, ordering) +func sortManifestsByKind(m []Manifest, ordering KindSortOrder) []Manifest { + sort.SliceStable(m, func(i, j int) bool { + return lessByKind(m[i], m[j], m[i].Head.Kind, m[j].Head.Kind, m[i].InstallBefore, m[j].InstallBefore, ordering) }) - return manifests + return m } // sort hooks by kind, using an out-of-place sort to preserve the input parameters. @@ -122,20 +122,15 @@ func sortManifestsByKind(manifests []Manifest, ordering KindSortOrder) []Manifes func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.Hook { h := hooks sort.SliceStable(h, func(i, j int) bool { - return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, ordering) + return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, nil, nil, ordering) }) return h } -func lessByKind(a interface{}, b interface{}, kindA string, kindB string, o KindSortOrder) bool { - ordering := make(map[string]int, len(o)) - for v, k := range o { - ordering[k] = v - } - - first, aok := ordering[kindA] - second, bok := ordering[kindB] +func lessByKind(a interface{}, b interface{}, kindA string, kindB string, beforeA []string, beforeB []string, o KindSortOrder) bool { + first, aok := installOrderIndex(kindA, beforeA, o) + second, bok := installOrderIndex(kindB, beforeB, o) if !aok && !bok { // if both are unknown then sort alphabetically by kind, keep original order if same kind @@ -154,3 +149,28 @@ func lessByKind(a interface{}, b interface{}, kindA string, kindB string, o Kind // sort different kinds, keep original order if same priority return first < second } + +// installOrderIndex returns the lowest index number of all beforeKinds +func installOrderIndex(kind string, beforeKinds []string, o KindSortOrder) (int, bool) { + ordering := make(map[string]int, len(o)) + for v, k := range o { + ordering[k] = v + } + + orderIndex, foundIndex := ordering[kind] + + // reset orderIndex for unknown resources + if !foundIndex { + orderIndex = len(o) + } + + for _, kind := range beforeKinds { + i, ok := ordering[kind] + if ok && i < orderIndex { + foundIndex = true + // set orderIndex 1 BEFORE the actual index, so it get executed BEFORE it + orderIndex = i - 1 + } + } + return orderIndex, foundIndex +} diff --git a/pkg/releaseutil/manifest.go b/pkg/releaseutil/manifest.go index 0b04a4599..34dd16be0 100644 --- a/pkg/releaseutil/manifest.go +++ b/pkg/releaseutil/manifest.go @@ -23,6 +23,9 @@ import ( "strings" ) +// InstallOrderAnnotation the annotation that allows manipulating the install order of custom resources +const InstallOrderAnnotation = "helm.sh/install-before" + // SimpleHead defines what the structure of the head of a manifest file type SimpleHead struct { Version string `json:"apiVersion"` diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go index e83414500..8f5f61263 100644 --- a/pkg/releaseutil/manifest_sorter.go +++ b/pkg/releaseutil/manifest_sorter.go @@ -32,9 +32,10 @@ import ( // Manifest represents a manifest file, which has a name and some content. type Manifest struct { - Name string - Content string - Head *SimpleHead + Name string + Content string + Head *SimpleHead + InstallBefore []string } // manifestFile represents a file that contains a manifest. @@ -155,6 +156,30 @@ func (file *manifestFile) sort(result *result) error { continue } + installBeforeKinds, ok := entry.Metadata.Annotations[InstallOrderAnnotation] + // InstallOrderAnnotation is only supported for unknown Kinds e.g. Custom Resources + if ok && isKnownKind(entry.Kind) { + log.Printf("info: %v annotation is not supported for Kind %v", InstallOrderAnnotation, entry.Kind) + } else if ok { + var installBefore []string + for _, kind := range strings.Split(installBeforeKinds, ",") { + kind = strings.TrimSpace(kind) + if isKnownKind(kind) { + installBefore = append(installBefore, kind) + } else { + log.Printf("info: skipping unknown install-before kind: %q", kind) + } + } + + result.generic = append(result.generic, Manifest{ + Name: file.path, + Content: m, + Head: &entry, + InstallBefore: installBefore, + }) + continue + } + hookTypes, ok := entry.Metadata.Annotations[release.HookAnnotation] if !ok { result.generic = append(result.generic, Manifest{ @@ -231,3 +256,15 @@ func operateAnnotationValues(entry SimpleHead, annotation string, operate func(p } } } + +// isKnownKind returns true if the given kind exists in the InstallOrder or UninstallOrder +func isKnownKind(kind string) bool { + knownKinds := append(InstallOrder, UninstallOrder...) + + for _, kk := range knownKinds { + if kk == kind { + return true + } + } + return false +} From 306b51b40bb1331e062ccabdea4c857d23e4b375 Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Wed, 24 Mar 2021 16:05:54 +0100 Subject: [PATCH 2/6] Sort uninstall kinds correctly Signed-off-by: Niklas Wagner --- pkg/action/action.go | 2 +- pkg/action/uninstall.go | 2 +- pkg/releaseutil/kind_sorter.go | 49 +++++++++++++++++++++--------- pkg/releaseutil/manifest_sorter.go | 4 +-- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index 79bb4f638..5d63aeffb 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -162,7 +162,7 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values // Sort hooks, manifests, and partials. Only hooks and manifests are returned, // as partials are not used after renderer.Render. Empty manifests are also // removed here. - hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) + hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, false) if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index c762159cb..3ccfcae32 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -181,7 +181,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) { } manifests := releaseutil.SplitManifests(rel.Manifest) - _, files, err := releaseutil.SortManifests(manifests, caps.APIVersions, releaseutil.UninstallOrder) + _, files, err := releaseutil.SortManifests(manifests, caps.APIVersions, true) if err != nil { // We could instead just delete everything in no particular order. // FIXME: One way to delete at this point would be to try a label-based diff --git a/pkg/releaseutil/kind_sorter.go b/pkg/releaseutil/kind_sorter.go index 89289c5de..3b063659d 100644 --- a/pkg/releaseutil/kind_sorter.go +++ b/pkg/releaseutil/kind_sorter.go @@ -17,6 +17,7 @@ limitations under the License. package releaseutil import ( + "log" "sort" "helm.sh/helm/v3/pkg/release" @@ -108,9 +109,10 @@ var UninstallOrder KindSortOrder = []string{ // sort manifests by kind. // // Results are sorted by 'ordering', keeping order of items with equal kind/priority -func sortManifestsByKind(m []Manifest, ordering KindSortOrder) []Manifest { +func sortManifestsByKind(manifests []Manifest, uninstall bool) []Manifest { + m := manifests sort.SliceStable(m, func(i, j int) bool { - return lessByKind(m[i], m[j], m[i].Head.Kind, m[j].Head.Kind, m[i].InstallBefore, m[j].InstallBefore, ordering) + return lessByKind(m[i], m[j], m[i].Head.Kind, m[j].Head.Kind, m[i].InstallBefore, m[j].InstallBefore, uninstall) }) return m @@ -119,18 +121,18 @@ func sortManifestsByKind(m []Manifest, ordering KindSortOrder) []Manifest { // sort hooks by kind, using an out-of-place sort to preserve the input parameters. // // Results are sorted by 'ordering', keeping order of items with equal kind/priority -func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.Hook { +func sortHooksByKind(hooks []*release.Hook, uninstall bool) []*release.Hook { h := hooks sort.SliceStable(h, func(i, j int) bool { - return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, nil, nil, ordering) + return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, []string{}, []string{}, uninstall) }) return h } -func lessByKind(a interface{}, b interface{}, kindA string, kindB string, beforeA []string, beforeB []string, o KindSortOrder) bool { - first, aok := installOrderIndex(kindA, beforeA, o) - second, bok := installOrderIndex(kindB, beforeB, o) +func lessByKind(a interface{}, b interface{}, kindA string, kindB string, beforeA []string, beforeB []string, uninstall bool) bool { + first, aok := installOrderIndex(kindA, beforeA, uninstall) + second, bok := installOrderIndex(kindB, beforeB, uninstall) if !aok && !bok { // if both are unknown then sort alphabetically by kind, keep original order if same kind @@ -151,26 +153,45 @@ func lessByKind(a interface{}, b interface{}, kindA string, kindB string, before } // installOrderIndex returns the lowest index number of all beforeKinds -func installOrderIndex(kind string, beforeKinds []string, o KindSortOrder) (int, bool) { - ordering := make(map[string]int, len(o)) - for v, k := range o { +func installOrderIndex(kind string, beforeKinds []string, uninstall bool) (int, bool) { + order := InstallOrder + if uninstall { + order = UninstallOrder + } + + ordering := make(map[string]int, len(order)) + for v, k := range order { ordering[k] = v } orderIndex, foundIndex := ordering[kind] // reset orderIndex for unknown resources - if !foundIndex { - orderIndex = len(o) + // when we're uninstalling we're actually searching for the HIGHEST index, so 0 is fine as initial value + if !foundIndex && !uninstall { + orderIndex = len(order) } for _, kind := range beforeKinds { i, ok := ordering[kind] - if ok && i < orderIndex { + if !ok { + continue + } + // we're searching for the lowest index when installing + if i < orderIndex && !uninstall { foundIndex = true - // set orderIndex 1 BEFORE the actual index, so it get executed BEFORE it + // set orderIndex 1 BEFORE the actual index, so it get installed BEFORE it orderIndex = i - 1 } + // we're searching for the highest index when uninstalling + if i > orderIndex && uninstall { + foundIndex = true + // set orderIndex 1 AFTER the actual index, so it get uninstalled AFTER it + orderIndex = i + 1 + } } + + log.Printf("kind: %v / beforeKinds: %v = %v (foundIndex: %v / uninstall: %v)", kind, beforeKinds, orderIndex, foundIndex, uninstall) + return orderIndex, foundIndex } diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go index 8f5f61263..1a1522898 100644 --- a/pkg/releaseutil/manifest_sorter.go +++ b/pkg/releaseutil/manifest_sorter.go @@ -76,7 +76,7 @@ var events = map[string]release.HookEvent{ // // Files that do not parse into the expected format are simply placed into a map and // returned. -func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { +func SortManifests(files map[string]string, apis chartutil.VersionSet, uninstall bool) ([]*release.Hook, []Manifest, error) { result := &result{} var sortedFilePaths []string @@ -109,7 +109,7 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering } } - return sortHooksByKind(result.hooks, ordering), sortManifestsByKind(result.generic, ordering), nil + return sortHooksByKind(result.hooks, uninstall), sortManifestsByKind(result.generic, uninstall), nil } // sort takes a manifestFile object which may contain multiple resource definition From 17eb1e756bb03126baf500324689e6c15389407f Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Wed, 24 Mar 2021 16:37:13 +0100 Subject: [PATCH 3/6] WIP: Fix existing tests Signed-off-by: Niklas Wagner --- pkg/releaseutil/kind_sorter_test.go | 27 +++++++++++++------------ pkg/releaseutil/manifest_sorter.go | 6 +++--- pkg/releaseutil/manifest_sorter_test.go | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/pkg/releaseutil/kind_sorter_test.go b/pkg/releaseutil/kind_sorter_test.go index 71d355210..907c7deac 100644 --- a/pkg/releaseutil/kind_sorter_test.go +++ b/pkg/releaseutil/kind_sorter_test.go @@ -18,6 +18,7 @@ package releaseutil import ( "bytes" + "reflect" "testing" "helm.sh/helm/v3/pkg/release" @@ -169,11 +170,11 @@ func TestKindSorter(t *testing.T) { for _, test := range []struct { description string - order KindSortOrder + uninstall bool expected string }{ - {"install", InstallOrder, "aAbcC3deEf1gh2iIjJkKlLmnopqrxstuvw!"}, - {"uninstall", UninstallOrder, "wvmutsxrqponLlKkJjIi2hg1fEed3CcbAa!"}, + {"install", false, "aAbcC3deEf1gh2iIjJkKlLmnopqrxstuvw!"}, + {"uninstall", true, "wvmutsxrqponLlKkJjIi2hg1fEed3CcbAa!"}, } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { @@ -182,14 +183,14 @@ func TestKindSorter(t *testing.T) { } defer buf.Reset() orig := manifests - for _, r := range sortManifestsByKind(manifests, test.order) { + for _, r := range sortManifestsByKind(manifests, test.uninstall) { buf.WriteString(r.Name) } if got := buf.String(); got != test.expected { t.Errorf("Expected %q, got %q", test.expected, got) } for i, manifest := range orig { - if manifest != manifests[i] { + if !reflect.DeepEqual(manifest, manifests[i]) { t.Fatal("Expected input to sortManifestsByKind to stay the same") } } @@ -239,16 +240,16 @@ func TestKindSorterKeepOriginalOrder(t *testing.T) { } for _, test := range []struct { description string - order KindSortOrder + uninstall bool expected string }{ // expectation is sorted by kind (unknown is last) and within each group of same kind, the order is kept - {"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01aAz!u2u1t3"}, + {"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", false, "01aAz!u2u1t3"}, } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { defer buf.Reset() - for _, r := range sortManifestsByKind(manifests, test.order) { + for _, r := range sortManifestsByKind(manifests, test.uninstall) { buf.WriteString(r.Name) } if got := buf.String(); got != test.expected { @@ -269,7 +270,7 @@ func TestKindSorterNamespaceAgainstUnknown(t *testing.T) { } manifests := []Manifest{unknown, namespace} - manifests = sortManifestsByKind(manifests, InstallOrder) + manifests = sortManifestsByKind(manifests, false) expectedOrder := []Manifest{namespace, unknown} for i, manifest := range manifests { @@ -302,11 +303,11 @@ func TestKindSorterForHooks(t *testing.T) { for _, test := range []struct { description string - order KindSortOrder + uninstall bool expected string }{ - {"install", InstallOrder, "acij"}, - {"uninstall", UninstallOrder, "jica"}, + {"install", false, "acij"}, + {"uninstall", true, "jica"}, } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { @@ -315,7 +316,7 @@ func TestKindSorterForHooks(t *testing.T) { } defer buf.Reset() orig := hooks - for _, r := range sortHooksByKind(hooks, test.order) { + for _, r := range sortHooksByKind(hooks, test.uninstall) { buf.WriteString(r.Name) } for i, hook := range orig { diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go index 1a1522898..9856d919e 100644 --- a/pkg/releaseutil/manifest_sorter.go +++ b/pkg/releaseutil/manifest_sorter.go @@ -164,11 +164,11 @@ func (file *manifestFile) sort(result *result) error { var installBefore []string for _, kind := range strings.Split(installBeforeKinds, ",") { kind = strings.TrimSpace(kind) - if isKnownKind(kind) { - installBefore = append(installBefore, kind) - } else { + if !isKnownKind(kind) { log.Printf("info: skipping unknown install-before kind: %q", kind) + continue } + installBefore = append(installBefore, kind) } result.generic = append(result.generic, Manifest{ diff --git a/pkg/releaseutil/manifest_sorter_test.go b/pkg/releaseutil/manifest_sorter_test.go index 20d809317..7cb8e5074 100644 --- a/pkg/releaseutil/manifest_sorter_test.go +++ b/pkg/releaseutil/manifest_sorter_test.go @@ -139,7 +139,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := SortManifests(manifests, chartutil.VersionSet{"v1", "v1beta1"}, InstallOrder) + hs, generic, err := SortManifests(manifests, chartutil.VersionSet{"v1", "v1beta1"}, false) if err != nil { t.Fatalf("Unexpected error: %s", err) } @@ -219,7 +219,7 @@ metadata: } } - sorted = sortManifestsByKind(sorted, InstallOrder) + sorted = sortManifestsByKind(sorted, false) for i, m := range generic { if m.Content != sorted[i].Content { t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content) From 5cdf1031911622744339cd451f756b9919d0beac Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Wed, 24 Mar 2021 17:23:10 +0100 Subject: [PATCH 4/6] Allow placing Custom Resources in between known resources Signed-off-by: Niklas Wagner --- pkg/releaseutil/kind_sorter.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/releaseutil/kind_sorter.go b/pkg/releaseutil/kind_sorter.go index 3b063659d..330936505 100644 --- a/pkg/releaseutil/kind_sorter.go +++ b/pkg/releaseutil/kind_sorter.go @@ -17,7 +17,6 @@ limitations under the License. package releaseutil import ( - "log" "sort" "helm.sh/helm/v3/pkg/release" @@ -161,7 +160,14 @@ func installOrderIndex(kind string, beforeKinds []string, uninstall bool) (int, ordering := make(map[string]int, len(order)) for v, k := range order { - ordering[k] = v + // In order to allow placing custom resources in between existing resources we need to double the index. + // for example + // NetworkPolicy has an index of 1 + // ResourceQuota has an index of 2 + // if we want to place a custom resource in between we would need to make the index of that resource 1.5 + // since we use int numbers we cannot use floating point numbers, so instead we just DOUBLE the index of + // everything so that our Custom Resource fits in between (2 and 4 in this case) + ordering[k] = v * 2 } orderIndex, foundIndex := ordering[kind] @@ -169,7 +175,8 @@ func installOrderIndex(kind string, beforeKinds []string, uninstall bool) (int, // reset orderIndex for unknown resources // when we're uninstalling we're actually searching for the HIGHEST index, so 0 is fine as initial value if !foundIndex && !uninstall { - orderIndex = len(order) + // see above why we use double the length + orderIndex = len(order) * 2 } for _, kind := range beforeKinds { @@ -191,7 +198,5 @@ func installOrderIndex(kind string, beforeKinds []string, uninstall bool) (int, } } - log.Printf("kind: %v / beforeKinds: %v = %v (foundIndex: %v / uninstall: %v)", kind, beforeKinds, orderIndex, foundIndex, uninstall) - return orderIndex, foundIndex } From 59b994daf637ed0c9d7a0e2f12e55b473bf75a58 Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Wed, 24 Mar 2021 17:23:19 +0100 Subject: [PATCH 5/6] Add additional test cases Signed-off-by: Niklas Wagner --- pkg/releaseutil/kind_sorter_test.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pkg/releaseutil/kind_sorter_test.go b/pkg/releaseutil/kind_sorter_test.go index 907c7deac..859069e3b 100644 --- a/pkg/releaseutil/kind_sorter_test.go +++ b/pkg/releaseutil/kind_sorter_test.go @@ -166,6 +166,21 @@ func TestKindSorter(t *testing.T) { Name: "x", Head: &SimpleHead{Kind: "HorizontalPodAutoscaler"}, }, + { + Name: "9", + Head: &SimpleHead{Kind: "MyCustomResourceBeforeDeployment"}, + InstallBefore: []string{"Deployment"}, + }, + { + Name: "8", + Head: &SimpleHead{Kind: "MyCustomResourceBeforeEverything"}, + InstallBefore: InstallOrder, + }, + { + Name: "7", + Head: &SimpleHead{Kind: "MyCustomResourceBeforeMultipleResources"}, + InstallBefore: []string{"Deployment", "Service", "ServiceAccount"}, + }, } for _, test := range []struct { @@ -173,8 +188,8 @@ func TestKindSorter(t *testing.T) { uninstall bool expected string }{ - {"install", false, "aAbcC3deEf1gh2iIjJkKlLmnopqrxstuvw!"}, - {"uninstall", true, "wvmutsxrqponLlKkJjIi2hg1fEed3CcbAa!"}, + {"install", false, "8aAbcC37deEf1gh2iIjJkKlLmnopq9rxstuvw!"}, + {"uninstall", true, "wvmutsxr9qponLlKkJjIi2hg1fEed73CcbAa8!"}, } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { From 03811bf5152a44528049f701b5d069abdb1a1168 Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Wed, 24 Mar 2021 17:42:38 +0100 Subject: [PATCH 6/6] Make sure that the Kind exist in Install and Uninstall Order Signed-off-by: Niklas Wagner --- pkg/releaseutil/manifest_sorter.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go index 9856d919e..97720cd4e 100644 --- a/pkg/releaseutil/manifest_sorter.go +++ b/pkg/releaseutil/manifest_sorter.go @@ -257,13 +257,15 @@ func operateAnnotationValues(entry SimpleHead, annotation string, operate func(p } } -// isKnownKind returns true if the given kind exists in the InstallOrder or UninstallOrder +// isKnownKind returns true if the given kind exists in the InstallOrder AND UninstallOrder func isKnownKind(kind string) bool { - knownKinds := append(InstallOrder, UninstallOrder...) - - for _, kk := range knownKinds { - if kk == kind { - return true + for _, k := range InstallOrder { + if k == kind { + for _, kk := range UninstallOrder { + if kk == kind { + return true + } + } } } return false