This is an automated email from the ASF dual-hosted git repository.

pbacsko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-core.git


The following commit(s) were added to refs/heads/master by this push:
     new d1589f9d [YUNIKORN-1816] Fixed placement rule no longer works due to 
incorrect validation (#572)
d1589f9d is described below

commit d1589f9d9032021e9aa545d91f6c211d8366abbc
Author: Peter Bacsko <[email protected]>
AuthorDate: Thu Jun 22 16:46:08 2023 +0200

    [YUNIKORN-1816] Fixed placement rule no longer works due to incorrect 
validation (#572)
    
    Closes: #572
    
    Signed-off-by: Peter Bacsko <[email protected]>
---
 pkg/common/configs/config_test.go          |   5 +-
 pkg/common/configs/configvalidator.go      | 173 +++++++++++++++++++----------
 pkg/common/configs/configvalidator_test.go | 139 ++++++++++++++++++++---
 3 files changed, 243 insertions(+), 74 deletions(-)

diff --git a/pkg/common/configs/config_test.go 
b/pkg/common/configs/config_test.go
index 7261a886..ca67a47a 100644
--- a/pkg/common/configs/config_test.go
+++ b/pkg/common/configs/config_test.go
@@ -732,9 +732,12 @@ partitions:
   - name: default
     queues:
       - name: root
+        queues:
+          - name: default
+            parent: false
     placementrules:
       - name: fixed
-        value: root
+        value: root.default
       - name: tag
         value: Just Any value
 `
diff --git a/pkg/common/configs/configvalidator.go 
b/pkg/common/configs/configvalidator.go
index 1652b0b3..ab7dc550 100644
--- a/pkg/common/configs/configvalidator.go
+++ b/pkg/common/configs/configvalidator.go
@@ -53,8 +53,16 @@ const (
        // app sort priority values
        ApplicationSortPriorityEnabled  = "enabled"
        ApplicationSortPriorityDisabled = "disabled"
+
+       // placement rule validation
+       placementOK placementPathCheckResult = iota
+       errNonExistingQueue
+       errQueueNotLeaf
+       errLastQueueLeaf
 )
 
+type placementPathCheckResult int
+
 // Priority
 var MinPriority int32 = math.MinInt32
 var MaxPriority int32 = math.MaxInt32
@@ -79,20 +87,13 @@ var SpecialRegExp = regexp.MustCompile(`[\^$*+?()\[{}|]`)
 var RuleNameRegExp = regexp.MustCompile(`^[_a-zA-Z][a-zA-Z0-9_]*$`)
 
 type placementStaticPath struct {
-       path      string
-       ruleChain string
-       create    bool
-       ruleNo    int
+       path           string
+       ruleChain      string
+       create         bool
+       hasDynamicPart bool
+       ruleNo         int
 }
 
-type placementPathCheckResult int
-
-const (
-       checkOK placementPathCheckResult = iota
-       nonExistingQueue
-       queueNotParent
-)
-
 // Check the ACL
 func checkACL(acl string) error {
        // trim any white space
@@ -295,33 +296,52 @@ func checkPlacementRules(partition *PartitionConfig) 
error {
                }
        }
 
-       placementStaticPaths := 
getLongestPlacementPaths(partition.PlacementRules)
+       placementStaticPaths, err := 
getLongestPlacementPaths(partition.PlacementRules)
+       if err != nil {
+               return err
+       }
        for _, staticPath := range placementStaticPaths {
                queuePath := staticPath.path
+               create := staticPath.create
+               hasDynamicPart := staticPath.hasDynamicPart
+
                parts := strings.Split(strings.ToLower(queuePath), DOT)
-               result := checkQueueHierarchyForPlacement(parts, 
staticPath.create, partition.Queues)
-               if result == queueNotParent {
-                       return fmt.Errorf("placement rule no. #%d (%s) 
references a queue (%s) which is a leaf",
+               result, lastQueue := checkQueueHierarchyForPlacement(parts, 
create, hasDynamicPart, partition.Queues, nil)
+               if result == errQueueNotLeaf {
+                       return fmt.Errorf("placement rule no. #%d (%s) 
references a queue (%s) which is not a leaf",
                                staticPath.ruleNo, staticPath.ruleChain, 
queuePath)
                }
-               if result == nonExistingQueue {
+               if result == errNonExistingQueue {
                        return fmt.Errorf("placement rule no. #%d (%s) 
references non-existing queues (%s) and create is 'false'",
                                staticPath.ruleNo, staticPath.ruleChain, 
queuePath)
                }
+               if result == errLastQueueLeaf {
+                       return fmt.Errorf("placement rule no. #%d (%s) 
references non-existing queues (%s) which cannot be created because the last 
queue (%s) in the hierarchy is a leaf",
+                               staticPath.ruleNo, staticPath.ruleChain, 
queuePath, lastQueue)
+               }
        }
 
        return nil
 }
 
-func checkQueueHierarchyForPlacement(path []string, create bool, conf 
[]QueueConfig) placementPathCheckResult {
+func checkQueueHierarchyForPlacement(path []string, create, hasDynamicPart 
bool, conf []QueueConfig, parentConf *QueueConfig) (result 
placementPathCheckResult, lastQueueName string) {
        queueName := path[0]
+       lastQueueName = ""
 
        // no more queues in the configuration
        if len(conf) == 0 {
+               if !parentConf.Parent {
+                       // path in the hierarchy is shorter, but the last queue 
is a leaf
+                       result = errLastQueueLeaf
+                       lastQueueName = parentConf.Name
+                       return
+               }
                if !create {
-                       return nonExistingQueue
+                       result = errNonExistingQueue
+                       return
                }
-               return checkOK
+               result = placementOK
+               return
        }
 
        var queueConf *QueueConfig
@@ -336,21 +356,34 @@ func checkQueueHierarchyForPlacement(path []string, 
create bool, conf []QueueCon
        // queue not found on this level
        if queueConf == nil {
                if !create {
-                       return nonExistingQueue
+                       result = errNonExistingQueue
+                       return
                }
-               return checkOK
-       }
-
-       if !queueConf.Parent {
-               return queueNotParent
+               result = placementOK
+               return
        }
 
        if len(path) == 1 {
-               return checkOK
+               if hasDynamicPart {
+                       // the "fixed" rule is followed by other rules like 
tag, user, etc. (root.dev.<user>),
+                       // which means that the "fixed" part must point to a 
parent
+                       if queueConf.Parent {
+                               result = placementOK
+                               return
+                       }
+                       result = errQueueNotLeaf
+                       return
+               }
+               if queueConf.Parent {
+                       result = errQueueNotLeaf
+                       return
+               }
+               result = placementOK
+               return
        }
 
        path = path[1:]
-       return checkQueueHierarchyForPlacement(path, create, queueConf.Queues)
+       return checkQueueHierarchyForPlacement(path, create, hasDynamicPart, 
queueConf.Queues, queueConf)
 }
 
 // Check the specific rule for syntax.
@@ -738,51 +771,79 @@ func Validate(newConfig *SchedulerConfig) error {
 }
 
 // returns the longest fixed queue path defined by the placement rule chain
-// e.g. the chain is user->tag->fixed, returns something like 
"root.users.<tag>.<user>",
+// e.g. the chain is fixed->tag->user, returns something like 
"root.users.<tag>.<user>",
 // the longest static part is "root.users"
-func getLongestPlacementPaths(rules []PlacementRule) []placementStaticPath {
+func getLongestPlacementPaths(rules []PlacementRule) ([]placementStaticPath, 
error) {
        paths := make([]placementStaticPath, 0)
 
        for i, rule := range rules {
-               path, ruleChain, _ := getLongestStaticPath(rule)
+               path, ruleChain, hasDynamicPart, err := 
getLongestStaticPath(rule)
+               if err != nil {
+                       return nil, err
+               }
+               if strings.Index(path, RootQueue) != 0 {
+                       continue
+               }
                placementPath := placementStaticPath{
-                       path:      path,
-                       create:    rule.Create,
-                       ruleChain: ruleChain,
-                       ruleNo:    i,
+                       path:           path,
+                       create:         rule.Create,
+                       ruleChain:      ruleChain,
+                       hasDynamicPart: hasDynamicPart,
+                       ruleNo:         i,
                }
                paths = append(paths, placementPath)
        }
 
-       return paths
+       return paths, nil
 }
 
-func getLongestStaticPath(rule PlacementRule) (string, string, bool) {
-       var parentPath string
-       var ruleChain string
-       dynamicParent := false
+func getLongestStaticPath(rule PlacementRule) (staticPath, ruleChain string, 
foundDynamicRule bool, err error) {
+       rules := getRuleChain(rule)
 
-       if rule.Parent != nil {
-               var chain string
-               parentPath, chain, dynamicParent = 
getLongestStaticPath(*rule.Parent)
-               ruleChain = rule.Name + "->" + chain
-       } else {
-               ruleChain = rule.Name
-               parentPath = RootQueue
-       }
+       for _, r := range rules {
+               if ruleChain == "" {
+                       ruleChain = r.Name
+               } else {
+                       ruleChain = ruleChain + "->" + r.Name
+               }
+               if foundDynamicRule {
+                       continue
+               }
 
-       if rule.Name == types.Fixed {
-               queueName := strings.ToLower(rule.Value)
+               if r.Name != types.Fixed {
+                       if staticPath == "" {
+                               staticPath = "<dynamic>"
+                       }
+                       foundDynamicRule = true
+                       continue
+               }
+
+               queueName := r.Value
                qualified := strings.HasPrefix(queueName, RootQueue)
                if qualified {
-                       return queueName, ruleChain, false
+                       if staticPath != "" {
+                               // error, only the first fixed rule can be 
fully qualified
+                               err = fmt.Errorf("illegal fully qualified 
'fixed' rule with value %s", queueName)
+                               return staticPath, ruleChain, foundDynamicRule, 
err
+                       }
+                       staticPath = queueName
+                       continue
                }
-               // there is a parent rule other than "fixed", we can't do 
anything about that
-               if dynamicParent {
-                       return RootQueue, ruleChain, true
+               if staticPath == "" {
+                       staticPath = RootQueue
                }
-               return parentPath + "." + queueName, ruleChain, false
+               staticPath = staticPath + "." + queueName
+       }
+
+       return staticPath, ruleChain, foundDynamicRule, nil
+}
+
+func getRuleChain(r PlacementRule) []PlacementRule {
+       rules := make([]PlacementRule, 0)
+       if r.Parent != nil {
+               rules = append(rules, getRuleChain(*r.Parent)...)
        }
 
-       return parentPath, ruleChain, true
+       rules = append(rules, r)
+       return rules
 }
diff --git a/pkg/common/configs/configvalidator_test.go 
b/pkg/common/configs/configvalidator_test.go
index 6c6b82b3..9b871899 100644
--- a/pkg/common/configs/configvalidator_test.go
+++ b/pkg/common/configs/configvalidator_test.go
@@ -170,38 +170,97 @@ func TestCheckQueueMaxApplicationsForQueue(t *testing.T) {
 }
 
 func TestGetLongestPlacementPath(t *testing.T) {
-       staticPaths := getLongestPlacementPaths(createPlacementRules())
+       staticPaths, err := getLongestPlacementPaths(createPlacementRules())
+       assert.NilError(t, err)
        assert.Equal(t, 2, len(staticPaths))
 
        path0 := staticPaths[0]
        assert.Equal(t, "root.users", path0.path)
        assert.Equal(t, 0, path0.ruleNo)
-       assert.Equal(t, "user->tag->fixed", path0.ruleChain)
+       assert.Equal(t, "fixed->tag->user", path0.ruleChain)
        assert.Equal(t, false, path0.create)
        path1 := staticPaths[1]
        assert.Equal(t, "root.admins.dev", path1.path)
        assert.Equal(t, 1, path1.ruleNo)
        assert.Equal(t, "fixed->fixed", path1.ruleChain)
        assert.Equal(t, true, path1.create)
+
+       // illegal: two "fixed" with fully qualified path
+       illegal := []PlacementRule{
+               {
+                       Name:   "fixed",
+                       Value:  "root.dev",
+                       Create: true,
+                       Parent: &PlacementRule{
+                               Name:  "fixed",
+                               Value: "root.admins",
+                       },
+               },
+       }
+       _, err = getLongestPlacementPaths(illegal)
+       assert.ErrorContains(t, err, "illegal fully qualified 'fixed' rule")
 }
 
 func TestCheckQueueHierarchyForPlacement(t *testing.T) {
        queues := createQueueConfig()
-       parts := strings.Split(strings.ToLower("root.users"), DOT)
-       result := checkQueueHierarchyForPlacement(parts, false, queues)
-       assert.Equal(t, checkOK, result)
-
-       parts = strings.Split(strings.ToLower("root.users.dev"), DOT)
-       result = checkQueueHierarchyForPlacement(parts, true, queues)
-       assert.Equal(t, checkOK, result)
 
+       // case #1 - referring to existing queue which is a parent
+       parts := strings.Split("root.users", DOT)
+       result, queueName := checkQueueHierarchyForPlacement(parts, false, 
false, queues, nil)
+       assert.Equal(t, errQueueNotLeaf, result)
+       assert.Equal(t, "", queueName)
+
+       // case #2 - referring to an existing queue which is a leaf
+       parts = strings.Split("root.default", DOT)
+       result, queueName = checkQueueHierarchyForPlacement(parts, true, false, 
queues, nil)
+       assert.Equal(t, placementOK, result)
+       assert.Equal(t, "", queueName)
+
+       // case #3 - referring a path which is incomplete in the hierarchy, 
"users" is parent, create = true
+       parts = strings.Split("root.users.alice", DOT)
+       result, queueName = checkQueueHierarchyForPlacement(parts, true, false, 
queues, nil)
+       assert.Equal(t, placementOK, result)
+       assert.Equal(t, "", queueName)
+
+       // case #4 - referring a path which is incomplete in the hierarchy, 
"users" is parent, create = false
+       result, queueName = checkQueueHierarchyForPlacement(parts, false, 
false, queues, nil)
+       assert.Equal(t, errNonExistingQueue, result)
+       assert.Equal(t, "", queueName)
+
+       // case #5 - referring a path which is incomplete in the hierarchy, 
"users" is leaf, create = true
        queues[0].Queues[0].Parent = false
-       result = checkQueueHierarchyForPlacement(parts, false, queues)
-       assert.Equal(t, queueNotParent, result)
-
+       result, queueName = checkQueueHierarchyForPlacement(parts, true, false, 
queues, nil)
+       assert.Equal(t, errLastQueueLeaf, result)
+       assert.Equal(t, "users", queueName)
+
+       // case #6 - referring a path which is incomplete in the hierarchy, 
"users" is leaf, create = false
+       result, queueName = checkQueueHierarchyForPlacement(parts, false, 
false, queues, nil)
+       assert.Equal(t, errLastQueueLeaf, result)
+       assert.Equal(t, "users", queueName)
+
+       // case #7 - hierarchy is long enough, but no matching queue found, 
create = true
+       parts = strings.Split("root.devs.test", DOT)
+       result, queueName = checkQueueHierarchyForPlacement(parts, true, false, 
queues, nil)
+       assert.Equal(t, placementOK, result)
+       assert.Equal(t, "", queueName)
+
+       // case #8 - hierarchy is long enough, but no matching queue found, 
create = false
+       result, queueName = checkQueueHierarchyForPlacement(parts, false, 
false, queues, nil)
+       assert.Equal(t, errNonExistingQueue, result)
+       assert.Equal(t, "", queueName)
+
+       // case #9 - rule chain ends with a dynamic part, last queue is a leaf
+       parts = strings.Split("root.users", DOT)
+       result, queueName = checkQueueHierarchyForPlacement(parts, false, true, 
queues, nil)
+       assert.Equal(t, errQueueNotLeaf, result)
+       assert.Equal(t, "", queueName)
+
+       // case #10 - rule chain ends with a dynamic part, last queue is a 
parent
        queues[0].Queues[0].Parent = true
-       result = checkQueueHierarchyForPlacement(parts, false, queues)
-       assert.Equal(t, nonExistingQueue, result)
+       parts = strings.Split("root.users", DOT)
+       result, queueName = checkQueueHierarchyForPlacement(parts, false, true, 
queues, nil)
+       assert.Equal(t, placementOK, result)
+       assert.Equal(t, "", queueName)
 }
 
 func TestCheckPlacementRules(t *testing.T) {
@@ -210,17 +269,49 @@ func TestCheckPlacementRules(t *testing.T) {
                Queues:         createQueueConfig(),
        }
 
+       // default case, no error
        err := checkPlacementRules(conf)
        assert.NilError(t, err)
 
-       conf.Queues[0].Queues[0].Parent = false
+       // referencing "root.users", but "users" is a leaf
+       conf.PlacementRules = []PlacementRule{
+               {
+                       Name:  "fixed",
+                       Value: "root.users",
+               },
+       }
        err = checkPlacementRules(conf)
-       assert.ErrorContains(t, err, "placement rule no. #0 (user->tag->fixed) 
references a queue (root.users) which is a leaf")
+       assert.ErrorContains(t, err, "placement rule no. #0 (fixed) references 
a queue (root.users) which is not a leaf")
 
-       conf.Queues[0].Queues[0].Parent = true
+       // referencing "root.admins.dev" which doesn't exist but 'create' is 
false
+       conf.PlacementRules = createPlacementRules()
        conf.PlacementRules[1].Create = false
        err = checkPlacementRules(conf)
        assert.ErrorContains(t, err, "placement rule no. #1 (fixed->fixed) 
references non-existing queues (root.admins.dev) and create is 'false'")
+
+       // referencing "root.default", but queues under "default" cannot be 
created due to "default" being leaf
+       conf.PlacementRules = []PlacementRule{
+               {
+                       Name:  "fixed",
+                       Value: "root.default.leaf",
+               },
+       }
+       err = checkPlacementRules(conf)
+       assert.ErrorContains(t, err, "placement rule no. #0 (fixed) references 
non-existing queues (root.default.leaf) which cannot be created because the 
last queue (default) in the hierarchy is a leaf")
+
+       // two "fixed" rule in a chain with both having fully qualified queues
+       conf.PlacementRules = []PlacementRule{
+               {
+                       Name:  "fixed",
+                       Value: "root.default.leaf",
+                       Parent: &PlacementRule{
+                               Name:  "fixed",
+                               Value: "root.default",
+                       },
+               },
+       }
+       err = checkPlacementRules(conf)
+       assert.ErrorContains(t, err, "illegal fully qualified 'fixed' rule with 
value root.default.leaf")
 }
 
 func createQueueConfig() []QueueConfig {
@@ -233,6 +324,20 @@ func createQueueConfig() []QueueConfig {
                                        Name:   "users",
                                        Parent: true,
                                },
+                               {
+                                       Name:   "devs",
+                                       Parent: true,
+                                       Queues: []QueueConfig{
+                                               {
+                                                       Name:   "yunikorn",
+                                                       Parent: true,
+                                               },
+                                       },
+                               },
+                               {
+                                       Name:   "default",
+                                       Parent: false,
+                               },
                                {
                                        Name:   "admins",
                                        Parent: true,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to