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

wilfreds 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 81d69749 [YUNIKORN-2581] Expose running placement rules in REST (#857)
81d69749 is described below

commit 81d697494d972d01c01135dd981e1ec2fc89f57d
Author: Wilfred Spiegelenburg <wilfr...@apache.org>
AuthorDate: Fri May 31 11:26:57 2024 +1000

    [YUNIKORN-2581] Expose running placement rules in REST (#857)
    
    Add a new REST endpoint under the partition that returns the currently
    active placement rules:
    /ws/v1/partition/:partition/placementrules
    The call retrieves the active set from the placement manager. This will
    include the recovery rule. The rule interface is extended with a new
    private method ruleDAO() that must be implemented by all rules and
    returns the RuleDAO object.
    
    Add the currently active set of placement rules to the state dump. This
    handles multiple partitions. It wraps the list of RuleDAO objects in a
    RuleDAOInfo per partition.
    
    Closes: #857
    
    Signed-off-by: Wilfred Spiegelenburg <wilfr...@apache.org>
---
 go.mod                                        |  1 +
 go.sum                                        |  2 +
 pkg/scheduler/partition.go                    |  9 ++-
 pkg/scheduler/placement/filter.go             | 47 ++++++++++++-
 pkg/scheduler/placement/filter_test.go        | 65 +++++++++++++-----
 pkg/scheduler/placement/fixed_rule.go         | 19 +++++
 pkg/scheduler/placement/fixed_rule_test.go    | 38 ++++++++++
 pkg/scheduler/placement/placement.go          | 23 +++++--
 pkg/scheduler/placement/placement_test.go     | 51 ++++++++++++--
 pkg/scheduler/placement/provided_rule.go      | 17 +++++
 pkg/scheduler/placement/provided_rule_test.go | 33 +++++++++
 pkg/scheduler/placement/recovery_rule.go      | 10 +++
 pkg/scheduler/placement/recovery_rule_test.go |  9 +++
 pkg/scheduler/placement/rule.go               | 44 ++++++++----
 pkg/scheduler/placement/rule_test.go          |  9 +++
 pkg/scheduler/placement/tag_rule.go           | 18 +++++
 pkg/scheduler/placement/tag_rule_test.go      | 38 ++++++++++
 pkg/scheduler/placement/testrule.go           | 17 +++++
 pkg/scheduler/placement/user_rule.go          | 17 +++++
 pkg/scheduler/placement/user_rule_test.go     | 33 +++++++++
 pkg/webservice/dao/rule_info.go               | 39 +++++++++++
 pkg/webservice/handlers.go                    | 32 +++++++++
 pkg/webservice/handlers_test.go               | 99 ++++++++++++++++++++++++---
 pkg/webservice/routes.go                      |  6 ++
 pkg/webservice/state_dump.go                  |  2 +
 25 files changed, 623 insertions(+), 55 deletions(-)

diff --git a/go.mod b/go.mod
index 65b2c5a8..5f180926 100644
--- a/go.mod
+++ b/go.mod
@@ -33,6 +33,7 @@ require (
        github.com/prometheus/common v0.45.0
        github.com/sasha-s/go-deadlock v0.3.1
        go.uber.org/zap v1.26.0
+       golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8
        golang.org/x/net v0.21.0
        golang.org/x/time v0.5.0
        google.golang.org/grpc v1.58.3
diff --git a/go.sum b/go.sum
index 35ee67e4..f04fef47 100644
--- a/go.sum
+++ b/go.sum
@@ -50,6 +50,8 @@ go.uber.org/multierr v1.10.0 
h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
 go.uber.org/multierr v1.10.0/go.mod 
h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
 go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 
h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
+golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod 
h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
 golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
 golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
diff --git a/pkg/scheduler/partition.go b/pkg/scheduler/partition.go
index 5662bd75..fc2c5404 100644
--- a/pkg/scheduler/partition.go
+++ b/pkg/scheduler/partition.go
@@ -481,14 +481,19 @@ func (pc *PartitionContext) getQueueInternal(name string) 
*objects.Queue {
        return queue
 }
 
-// Get the queue info for the whole queue structure to pass to the webservice
+// GetPartitionQueues builds the queue info for the whole queue structure to 
pass to the webservice
 func (pc *PartitionContext) GetPartitionQueues() dao.PartitionQueueDAOInfo {
        partitionQueueDAOInfo := pc.root.GetPartitionQueueDAOInfo(true)
        partitionQueueDAOInfo.Partition = 
common.GetPartitionNameWithoutClusterID(pc.Name)
        return partitionQueueDAOInfo
 }
 
-// Create the recovery queue.
+// GetPlacementRules returns the current active rule set as dao to expose to 
the webservice
+func (pc *PartitionContext) GetPlacementRules() []*dao.RuleDAO {
+       return pc.getPlacementManager().GetRulesDAO()
+}
+
+// createRecoveryQueue creates the recovery queue to add to the hierarchy
 func (pc *PartitionContext) createRecoveryQueue() (*objects.Queue, error) {
        return objects.NewRecoveryQueue(pc.root)
 }
diff --git a/pkg/scheduler/placement/filter.go 
b/pkg/scheduler/placement/filter.go
index 72f8181f..df94a002 100644
--- a/pkg/scheduler/placement/filter.go
+++ b/pkg/scheduler/placement/filter.go
@@ -22,10 +22,17 @@ import (
        "regexp"
 
        "go.uber.org/zap"
+       "golang.org/x/exp/maps"
 
        "github.com/apache/yunikorn-core/pkg/common/configs"
        "github.com/apache/yunikorn-core/pkg/common/security"
        "github.com/apache/yunikorn-core/pkg/log"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
+)
+
+const (
+       filterAllow = "allow"
+       filterDeny  = "deny"
 )
 
 type Filter struct {
@@ -51,7 +58,7 @@ func (filter Filter) allowUser(userObj security.UserGroup) 
bool {
        // if we have found the user in the list stop looking and return
        if filteredUser {
                log.Log(log.Config).Debug("Filter matched user getName", 
zap.String("user", user))
-               return filteredUser && filter.allow
+               return filter.allow
        }
        // not in the user list, check the groups in the list
        // walk over the list first match is taken
@@ -59,7 +66,7 @@ func (filter Filter) allowUser(userObj security.UserGroup) 
bool {
                filteredUser = filter.filterGroup(group)
                if filteredUser {
                        log.Log(log.Config).Debug("Filter matched user group", 
zap.String("group", group))
-                       return filteredUser && filter.allow
+                       return filter.allow
                }
        }
        return !filter.allow
@@ -85,6 +92,40 @@ func (filter Filter) filterGroup(group string) bool {
        return filter.groupList[group]
 }
 
+// filterDAO returns the DAO object for the filter.
+// Returns nil if the filter is considered "empty"
+func (filter Filter) filterDAO() *dao.FilterDAO {
+       // do not render an empty filter in the DAO
+       if filter.empty {
+               return nil
+       }
+       ft := filterAllow
+       if !filter.allow {
+               ft = filterDeny
+       }
+       var userList, groupList []string
+       if len(filter.userList) != 0 {
+               userList = maps.Keys(filter.userList)
+       }
+       if len(filter.groupList) != 0 {
+               groupList = maps.Keys(filter.groupList)
+       }
+       var userExp, groupExp string
+       if filter.userExp != nil {
+               userExp = filter.userExp.String()
+       }
+       if filter.groupExp != nil {
+               groupExp = filter.groupExp.String()
+       }
+       return &dao.FilterDAO{
+               Type:      ft,
+               UserList:  userList,
+               GroupList: groupList,
+               UserExp:   userExp,
+               GroupExp:  groupExp,
+       }
+}
+
 // Create a new filter based on the checked config
 // There should be no errors as the config is syntax checked before we get to 
this point.
 func newFilter(conf configs.Filter) Filter {
@@ -94,7 +135,7 @@ func newFilter(conf configs.Filter) Filter {
                empty:     true,
        }
        // type can only be '' , allow or deny.
-       filter.allow = conf.Type != "deny"
+       filter.allow = conf.Type != filterDeny
 
        var err error
        // create the user list or regexp
diff --git a/pkg/scheduler/placement/filter_test.go 
b/pkg/scheduler/placement/filter_test.go
index 9c2011ae..99afc9bb 100644
--- a/pkg/scheduler/placement/filter_test.go
+++ b/pkg/scheduler/placement/filter_test.go
@@ -19,16 +19,19 @@
 package placement
 
 import (
+       "reflect"
+       "regexp"
        "testing"
 
        "github.com/apache/yunikorn-core/pkg/common/configs"
        "github.com/apache/yunikorn-core/pkg/common/security"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 func TestNewFilterLists(t *testing.T) {
        // test simple no user or group: allow
        conf := configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
 
        filter := newFilter(conf)
        if !filter.allow {
@@ -46,7 +49,7 @@ func TestNewFilterLists(t *testing.T) {
 
        // test simple no user or group: deny
        conf = configs.Filter{}
-       conf.Type = "deny"
+       conf.Type = filterDeny
 
        filter = newFilter(conf)
        if filter.allow {
@@ -64,7 +67,7 @@ func TestNewFilterLists(t *testing.T) {
 
        // test simple empty lists
        conf = configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
        conf.Users = []string{}
        conf.Groups = []string{}
 
@@ -124,7 +127,7 @@ func TestNewFilterLists(t *testing.T) {
 func TestNewFilterExpressions(t *testing.T) {
        // test expression
        conf := configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
        conf.Users = []string{"user*"}
        conf.Groups = []string{"group[1-9]"}
 
@@ -271,7 +274,7 @@ func TestAllowUser(t *testing.T) {
        }
        // test deny user list
        conf := configs.Filter{}
-       conf.Type = "deny"
+       conf.Type = filterDeny
        conf.Users = []string{"user1"}
 
        filter := newFilter(conf)
@@ -286,7 +289,7 @@ func TestAllowUser(t *testing.T) {
 
        // test allow user list
        conf = configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
        conf.Users = []string{"user1"}
 
        filter = newFilter(conf)
@@ -301,7 +304,7 @@ func TestAllowUser(t *testing.T) {
 
        // test deny user exp
        conf = configs.Filter{}
-       conf.Type = "deny"
+       conf.Type = filterDeny
        conf.Users = []string{"user[0-9]"}
 
        filter = newFilter(conf)
@@ -316,7 +319,7 @@ func TestAllowUser(t *testing.T) {
 
        // test allow user exp
        conf = configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
        conf.Users = []string{"user[0-9]"}
 
        filter = newFilter(conf)
@@ -340,7 +343,7 @@ func TestAllowGroup(t *testing.T) {
 
        // test deny group list
        conf := configs.Filter{}
-       conf.Type = "deny"
+       conf.Type = filterDeny
        conf.Groups = []string{"group1"}
 
        filter := newFilter(conf)
@@ -355,7 +358,7 @@ func TestAllowGroup(t *testing.T) {
 
        // test allow group list
        conf = configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
        conf.Groups = []string{"group1"}
 
        filter = newFilter(conf)
@@ -370,7 +373,7 @@ func TestAllowGroup(t *testing.T) {
 
        // test deny group exp
        conf = configs.Filter{}
-       conf.Type = "deny"
+       conf.Type = filterDeny
        conf.Groups = []string{"group[0-9]"}
 
        filter = newFilter(conf)
@@ -385,7 +388,7 @@ func TestAllowGroup(t *testing.T) {
 
        // test allow group exp
        conf = configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
        conf.Groups = []string{"group[0-9]"}
 
        filter = newFilter(conf)
@@ -409,7 +412,7 @@ func TestAllowSecondaryGroup(t *testing.T) {
 
        // test deny group list
        conf := configs.Filter{}
-       conf.Type = "deny"
+       conf.Type = filterDeny
        conf.Groups = []string{"group2"}
 
        filter := newFilter(conf)
@@ -420,7 +423,7 @@ func TestAllowSecondaryGroup(t *testing.T) {
 
        // test allow group list
        conf = configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
        conf.Groups = []string{"group1", "group2"}
 
        filter = newFilter(conf)
@@ -431,7 +434,7 @@ func TestAllowSecondaryGroup(t *testing.T) {
 
        // test deny group exp
        conf = configs.Filter{}
-       conf.Type = "deny"
+       conf.Type = filterDeny
        conf.Groups = []string{"group[0-9]"}
 
        filter = newFilter(conf)
@@ -442,7 +445,7 @@ func TestAllowSecondaryGroup(t *testing.T) {
 
        // test allow group exp
        conf = configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
        conf.Groups = []string{"group[0-9]"}
 
        filter = newFilter(conf)
@@ -469,7 +472,7 @@ func TestAllowNoLists(t *testing.T) {
        }
        // test default allow behaviour (no filter)
        conf = configs.Filter{}
-       conf.Type = "allow"
+       conf.Type = filterAllow
 
        filter = newFilter(conf)
        if !filter.allowUser(userObj) {
@@ -477,10 +480,36 @@ func TestAllowNoLists(t *testing.T) {
        }
        // test default deny behaviour (no filter)
        conf = configs.Filter{}
-       conf.Type = "deny"
+       conf.Type = filterDeny
 
        filter = newFilter(conf)
        if filter.allowUser(userObj) {
                t.Error("deny filter type only did not deny user")
        }
 }
+
+func TestFilter_filterDAO(t *testing.T) {
+       // filters are tested also from each rule in different combinations
+       // this does the outliers and cases that should not happen
+       reg := regexp.MustCompile("^.*$")
+       tests := []struct {
+               name   string
+               filter Filter
+               want   *dao.FilterDAO
+       }{
+               {"empty", Filter{empty: true}, nil},
+               {"empty", Filter{}, &dao.FilterDAO{Type: filterDeny}},
+               {
+                       "everything",
+                       Filter{allow: true, userList: map[string]bool{"user": 
true}, groupList: map[string]bool{"group": true}, userExp: reg, groupExp: reg},
+                       &dao.FilterDAO{Type: filterAllow, UserList: 
[]string{"user"}, GroupList: []string{"group"}, UserExp: "^.*$", GroupExp: 
"^.*$"},
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if got := tt.filter.filterDAO(); 
!reflect.DeepEqual(got, tt.want) {
+                               t.Errorf("filterDAO() = %v, want %v", got, 
tt.want)
+                       }
+               })
+       }
+}
diff --git a/pkg/scheduler/placement/fixed_rule.go 
b/pkg/scheduler/placement/fixed_rule.go
index b62abd51..3e2e9216 100644
--- a/pkg/scheduler/placement/fixed_rule.go
+++ b/pkg/scheduler/placement/fixed_rule.go
@@ -20,6 +20,7 @@ package placement
 
 import (
        "fmt"
+       "strconv"
        "strings"
 
        "go.uber.org/zap"
@@ -28,6 +29,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/log"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
        "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 // A rule to place an application based on the queue in the configuration.
@@ -44,6 +46,23 @@ func (fr *fixedRule) getName() string {
        return types.Fixed
 }
 
+func (fr *fixedRule) ruleDAO() *dao.RuleDAO {
+       var pDAO *dao.RuleDAO
+       if fr.parent != nil {
+               pDAO = fr.parent.ruleDAO()
+       }
+       return &dao.RuleDAO{
+               Name: fr.getName(),
+               Parameters: map[string]string{
+                       "queue":     fr.queue,
+                       "create":    strconv.FormatBool(fr.create),
+                       "qualified": strconv.FormatBool(fr.qualified),
+               },
+               ParentRule: pDAO,
+               Filter:     fr.filter.filterDAO(),
+       }
+}
+
 func (fr *fixedRule) initialise(conf configs.PlacementRule) error {
        fr.queue = normalise(conf.Value)
        if fr.queue == "" {
diff --git a/pkg/scheduler/placement/fixed_rule_test.go 
b/pkg/scheduler/placement/fixed_rule_test.go
index 5067f93e..c1d1e6cc 100644
--- a/pkg/scheduler/placement/fixed_rule_test.go
+++ b/pkg/scheduler/placement/fixed_rule_test.go
@@ -25,6 +25,7 @@ import (
 
        "github.com/apache/yunikorn-core/pkg/common/configs"
        "github.com/apache/yunikorn-core/pkg/common/security"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 func TestFixedRule(t *testing.T) {
@@ -248,3 +249,40 @@ func TestFixedRuleParent(t *testing.T) {
                t.Errorf("fixed rule with parent declared as leaf should have 
failed '%s', error %v", queue, err)
        }
 }
+
+func Test_fixedRule_ruleDAO(t *testing.T) {
+       tests := []struct {
+               name string
+               conf configs.PlacementRule
+               want *dao.RuleDAO
+       }{
+               {
+                       "base",
+                       configs.PlacementRule{Name: "fixed", Value: "default"},
+                       &dao.RuleDAO{Name: "fixed", Parameters: 
map[string]string{"queue": "default", "qualified": "false", "create": "false"}},
+               },
+               {
+                       "qualified",
+                       configs.PlacementRule{Name: "fixed", Value: 
"root.default"},
+                       &dao.RuleDAO{Name: "fixed", Parameters: 
map[string]string{"queue": "root.default", "qualified": "true", "create": 
"false"}},
+               },
+               {
+                       "parent",
+                       configs.PlacementRule{Name: "fixed", Value: "default", 
Create: true, Parent: &configs.PlacementRule{Name: "test", Create: true}},
+                       &dao.RuleDAO{Name: "fixed", Parameters: 
map[string]string{"queue": "default", "qualified": "false", "create": "true"}, 
ParentRule: &dao.RuleDAO{Name: "test", Parameters: map[string]string{"create": 
"true"}}},
+               },
+               {
+                       "filter",
+                       configs.PlacementRule{Name: "fixed", Value: "default", 
Create: true, Filter: configs.Filter{Type: filterAllow, Groups: 
[]string{"group1", "group2"}}},
+                       &dao.RuleDAO{Name: "fixed", Parameters: 
map[string]string{"queue": "default", "qualified": "false", "create": "true"}, 
Filter: &dao.FilterDAO{Type: filterAllow, GroupList: []string{"group1", 
"group2"}}},
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       ur, err := newRule(tt.conf)
+                       assert.NilError(t, err, "setting up the rule failed")
+                       ruleDAO := ur.ruleDAO()
+                       assert.DeepEqual(t, tt.want, ruleDAO)
+               })
+       }
+}
diff --git a/pkg/scheduler/placement/placement.go 
b/pkg/scheduler/placement/placement.go
index 4aad4fce..196ccfd1 100644
--- a/pkg/scheduler/placement/placement.go
+++ b/pkg/scheduler/placement/placement.go
@@ -30,6 +30,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/log"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
        "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 // RejectedError is the standard error returned if placement has failed
@@ -52,10 +53,19 @@ func NewPlacementManager(rules []configs.PlacementRule, 
queueFunc func(string) *
        return m
 }
 
-// Update the rules for an active placement manager
-// Note that this will only be called when the manager is created earlier and 
the config is updated.
+// GetRulesDAO returns a list of RuleDAO objects of the configured rules
+func (m *AppPlacementManager) GetRulesDAO() []*dao.RuleDAO {
+       m.RLock()
+       defer m.RUnlock()
+       info := make([]*dao.RuleDAO, len(m.rules))
+       for i, r := range m.rules {
+               info[i] = r.ruleDAO()
+       }
+       return info
+}
+
+// UpdateRules sets the rules for an active placement manager
 func (m *AppPlacementManager) UpdateRules(rules []configs.PlacementRule) error 
{
-       log.Log(log.Config).Info("Building new rule list for placement manager")
        if err := m.initialise(rules); err != nil {
                log.Log(log.Config).Info("Placement manager rules not 
reloaded", zap.Error(err))
                return err
@@ -63,7 +73,7 @@ func (m *AppPlacementManager) UpdateRules(rules 
[]configs.PlacementRule) error {
        return nil
 }
 
-// Initialise the rules from a parsed config.
+// initialise the rules from a parsed config.
 func (m *AppPlacementManager) initialise(rules []configs.PlacementRule) error {
        log.Log(log.Config).Info("Building new rule list for placement manager")
        // build temp list from new config
@@ -85,6 +95,9 @@ func (m *AppPlacementManager) initialise(rules 
[]configs.PlacementRule) error {
        return nil
 }
 
+// PlaceApplication executes the rules for the passed in application.
+// On success the queueName of the application is set to the queue the 
application wil run in.
+// On failure the queueName is set to "" and an error is returned.
 func (m *AppPlacementManager) PlaceApplication(app *objects.Application) error 
{
        m.RLock()
        defer m.RUnlock()
@@ -174,7 +187,7 @@ func (m *AppPlacementManager) PlaceApplication(app 
*objects.Application) error {
        return nil
 }
 
-// Build the rule set based on the config.
+// buildRules builds a new rule set based on the config.
 // If the rule set is correct and can be used the new set is returned.
 // If any error is encountered a nil array is returned and the error set
 func buildRules(rules []configs.PlacementRule) ([]rule, error) {
diff --git a/pkg/scheduler/placement/placement_test.go 
b/pkg/scheduler/placement/placement_test.go
index b70efc07..14fe6ace 100644
--- a/pkg/scheduler/placement/placement_test.go
+++ b/pkg/scheduler/placement/placement_test.go
@@ -35,7 +35,7 @@ import (
 func TestManagerNew(t *testing.T) {
        // basic info without rules, manager should not init
        man := NewPlacementManager(nil, queueFunc)
-       assert.Equal(t, 2, len(man.rules), "wrong rule count for empty 
placement manager")
+       assert.Equal(t, 2, len(man.rules), "wrong rule count for new placement 
manager, no config")
        assert.Equal(t, types.Provided, man.rules[0].getName(), "wrong name for 
implicit provided rule")
        assert.Equal(t, types.Recovery, man.rules[1].getName(), "wrong name for 
implicit recovery rule")
 }
@@ -43,13 +43,21 @@ func TestManagerNew(t *testing.T) {
 func TestManagerInit(t *testing.T) {
        // basic info without rules, manager should implicitly init
        man := NewPlacementManager(nil, queueFunc)
-       assert.Equal(t, 2, len(man.rules), "wrong rule count for nil placement 
manager")
+       assert.Equal(t, 2, len(man.rules), "wrong rule count for nil rules 
config")
+       ruleDAOs := man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "wrong DAO count for nil rules 
config")
+       assert.Equal(t, ruleDAOs[0].Name, types.Provided, "expected provided 
rule as first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 
        // try to init with empty list should do the same
        var rules []configs.PlacementRule
        err := man.initialise(rules)
        assert.NilError(t, err, "Failed to initialize empty placement rules")
-       assert.Equal(t, 2, len(man.rules), "wrong rule count for empty 
placement manager")
+       assert.Equal(t, 2, len(man.rules), "wrong rule count for empty rules 
config")
+       ruleDAOs = man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "wrong DAO count for empty rules 
config")
+       assert.Equal(t, ruleDAOs[0].Name, types.Provided, "expected provided 
rule as first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 
        rules = []configs.PlacementRule{
                {Name: "unknown"},
@@ -58,6 +66,11 @@ func TestManagerInit(t *testing.T) {
        if err == nil {
                t.Error("initialise with 'unknown' rule list should have 
failed")
        }
+       // rules should not have changed
+       ruleDAOs = man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "unexpected change: wrong DAO count 
for failed reinit")
+       assert.Equal(t, ruleDAOs[0].Name, types.Provided, "expected provided 
rule as first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 
        // init the manager with one rule
        rules = []configs.PlacementRule{
@@ -65,17 +78,29 @@ func TestManagerInit(t *testing.T) {
        }
        err = man.initialise(rules)
        assert.NilError(t, err, "failed to init existing manager")
+       ruleDAOs = man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "wrong DAO count for manager with 
test rule")
+       assert.Equal(t, ruleDAOs[0].Name, types.Test, "expected test rule as 
first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 
        // update the manager: remove rules implicit state is reverted
        rules = []configs.PlacementRule{}
        err = man.initialise(rules)
        assert.NilError(t, err, "Failed to re-initialize empty placement rules")
-       assert.Equal(t, 2, len(man.rules), "wrong rule count for newly empty 
placement manager")
+       assert.Equal(t, 2, len(man.rules), "wrong rule count for empty rules 
config")
+       ruleDAOs = man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "wrong DAO count for empty rules 
list")
+       assert.Equal(t, ruleDAOs[0].Name, types.Provided, "expected provided 
rule as first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 
        // check if we handle a nil list
        err = man.initialise(nil)
        assert.NilError(t, err, "Failed to re-initialize nil placement rules")
-       assert.Equal(t, 2, len(man.rules), "wrong rule count for nil placement 
manager")
+       assert.Equal(t, 2, len(man.rules), "wrong rule count for nil rules 
config")
+       ruleDAOs = man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "wrong DAO count for nil rules 
config")
+       assert.Equal(t, ruleDAOs[0].Name, types.Provided, "expected provided 
rule as first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 }
 
 func TestManagerUpdate(t *testing.T) {
@@ -87,17 +112,29 @@ func TestManagerUpdate(t *testing.T) {
        }
        err := man.UpdateRules(rules)
        assert.NilError(t, err, "failed to update existing manager")
+       ruleDAOs := man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "wrong DAO count for manager with 
test rule")
+       assert.Equal(t, ruleDAOs[0].Name, types.Test, "expected test rule as 
first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 
        // update the manager: remove rules init state is reverted
        rules = []configs.PlacementRule{}
        err = man.UpdateRules(rules)
        assert.NilError(t, err, "Failed to re-initialize empty placement rules")
-       assert.Equal(t, 2, len(man.rules), "wrong rule count for newly empty 
placement manager")
+       assert.Equal(t, 2, len(man.rules), "wrong rule count for empty rules 
config")
+       ruleDAOs = man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "wrong DAO count for empty rules 
config")
+       assert.Equal(t, ruleDAOs[0].Name, types.Provided, "expected provided 
rule as first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 
        // check if we handle a nil list
        err = man.UpdateRules(nil)
        assert.NilError(t, err, "Failed to re-initialize nil placement rules")
-       assert.Equal(t, 2, len(man.rules), "wrong rule count for nil placement 
manager")
+       assert.Equal(t, 2, len(man.rules), "wrong rule count for nil rules 
config")
+       ruleDAOs = man.GetRulesDAO()
+       assert.Equal(t, 2, len(ruleDAOs), "wrong DAO count for nil rules 
config")
+       assert.Equal(t, ruleDAOs[0].Name, types.Provided, "expected provided 
rule as first rule")
+       assert.Equal(t, ruleDAOs[1].Name, types.Recovery, "expected recovery 
rule as second rule")
 }
 
 func TestManagerBuildRule(t *testing.T) {
diff --git a/pkg/scheduler/placement/provided_rule.go 
b/pkg/scheduler/placement/provided_rule.go
index 27974a78..834ff83c 100644
--- a/pkg/scheduler/placement/provided_rule.go
+++ b/pkg/scheduler/placement/provided_rule.go
@@ -20,6 +20,7 @@ package placement
 
 import (
        "fmt"
+       "strconv"
        "strings"
 
        "go.uber.org/zap"
@@ -28,6 +29,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/log"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
        "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 // A rule to place an application based on the queue provided by the user on 
submission.
@@ -42,6 +44,21 @@ func (pr *providedRule) getName() string {
        return types.Provided
 }
 
+func (pr *providedRule) ruleDAO() *dao.RuleDAO {
+       var pDAO *dao.RuleDAO
+       if pr.parent != nil {
+               pDAO = pr.parent.ruleDAO()
+       }
+       return &dao.RuleDAO{
+               Name: pr.getName(),
+               Parameters: map[string]string{
+                       "create": strconv.FormatBool(pr.create),
+               },
+               ParentRule: pDAO,
+               Filter:     pr.filter.filterDAO(),
+       }
+}
+
 func (pr *providedRule) initialise(conf configs.PlacementRule) error {
        pr.create = conf.Create
        pr.filter = newFilter(conf.Filter)
diff --git a/pkg/scheduler/placement/provided_rule_test.go 
b/pkg/scheduler/placement/provided_rule_test.go
index 5077c834..1a14d8f1 100644
--- a/pkg/scheduler/placement/provided_rule_test.go
+++ b/pkg/scheduler/placement/provided_rule_test.go
@@ -25,6 +25,7 @@ import (
 
        "github.com/apache/yunikorn-core/pkg/common/configs"
        "github.com/apache/yunikorn-core/pkg/common/security"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 func TestProvidedRulePlace(t *testing.T) {
@@ -207,3 +208,35 @@ func TestProvidedRuleParent(t *testing.T) {
                t.Errorf("provided rule placed app in incorrect queue '%s', err 
%v", queue, err)
        }
 }
+
+func Test_providedRule_ruleDAO(t *testing.T) {
+       tests := []struct {
+               name string
+               conf configs.PlacementRule
+               want *dao.RuleDAO
+       }{
+               {
+                       "base",
+                       configs.PlacementRule{Name: "provided"},
+                       &dao.RuleDAO{Name: "provided", Parameters: 
map[string]string{"create": "false"}},
+               },
+               {
+                       "parent",
+                       configs.PlacementRule{Name: "provided", Create: true, 
Parent: &configs.PlacementRule{Name: "test", Create: true}},
+                       &dao.RuleDAO{Name: "provided", Parameters: 
map[string]string{"create": "true"}, ParentRule: &dao.RuleDAO{Name: "test", 
Parameters: map[string]string{"create": "true"}}},
+               },
+               {
+                       "filter",
+                       configs.PlacementRule{Name: "provided", Create: false, 
Filter: configs.Filter{Type: filterDeny, Users: []string{"john*"}}},
+                       &dao.RuleDAO{Name: "provided", Parameters: 
map[string]string{"create": "false"}, Filter: &dao.FilterDAO{Type: filterDeny, 
UserExp: "john*"}},
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       ur, err := newRule(tt.conf)
+                       assert.NilError(t, err, "setting up the rule failed")
+                       ruleDAO := ur.ruleDAO()
+                       assert.DeepEqual(t, tt.want, ruleDAO)
+               })
+       }
+}
diff --git a/pkg/scheduler/placement/recovery_rule.go 
b/pkg/scheduler/placement/recovery_rule.go
index 4e2101b8..f1f6d725 100644
--- a/pkg/scheduler/placement/recovery_rule.go
+++ b/pkg/scheduler/placement/recovery_rule.go
@@ -26,6 +26,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/log"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
        "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 // A rule to place an application into the recovery queue if no other rules 
matched and application submission is forced.
@@ -39,6 +40,15 @@ func (rr *recoveryRule) getName() string {
        return types.Recovery
 }
 
+func (rr *recoveryRule) ruleDAO() *dao.RuleDAO {
+       return &dao.RuleDAO{
+               Name: rr.getName(),
+               Parameters: map[string]string{
+                       "queue": common.RecoveryQueueFull,
+               },
+       }
+}
+
 func (rr *recoveryRule) initialise(_ configs.PlacementRule) error {
        // no configuration needed for the recovery rule
        return nil
diff --git a/pkg/scheduler/placement/recovery_rule_test.go 
b/pkg/scheduler/placement/recovery_rule_test.go
index 75f005da..61c0d443 100644
--- a/pkg/scheduler/placement/recovery_rule_test.go
+++ b/pkg/scheduler/placement/recovery_rule_test.go
@@ -26,6 +26,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/common"
        "github.com/apache/yunikorn-core/pkg/common/configs"
        "github.com/apache/yunikorn-core/pkg/common/security"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
        siCommon "github.com/apache/yunikorn-scheduler-interface/lib/go/common"
 )
 
@@ -75,3 +76,11 @@ partitions:
                t.Errorf("recovery rule did not place forced application into 
recovery queue, resolved queue '%s', err %v ", queue, err)
        }
 }
+
+func Test_recoveryRule_ruleDAO(t *testing.T) {
+       // nothing should be set as everything is ignored by the rule
+       rrDAO := &dao.RuleDAO{Name: "recovery", Parameters: 
map[string]string{"queue": common.RecoveryQueueFull}}
+       rr := &recoveryRule{}
+       ruleDAO := rr.ruleDAO()
+       assert.DeepEqual(t, rrDAO, ruleDAO)
+}
diff --git a/pkg/scheduler/placement/rule.go b/pkg/scheduler/placement/rule.go
index 1816a52d..cdc542e1 100644
--- a/pkg/scheduler/placement/rule.go
+++ b/pkg/scheduler/placement/rule.go
@@ -28,6 +28,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/log"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
        "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 // Interface that all placement rules need to implement.
@@ -48,6 +49,10 @@ type rule interface {
        // Return the parent rule.
        // This method is implemented in the basicRule which each rule must be 
based on.
        getParent() rule
+
+       // Returns the rule in a form that can be exposed via the REST api
+       // This method is implemented in the basicRule which each rule must be 
based on.
+       ruleDAO() *dao.RuleDAO
 }
 
 // Basic structure that every placement rule uses.
@@ -61,13 +66,13 @@ type basicRule struct {
        filter Filter
 }
 
-// Get the parent rule used in testing only.
+// getParent gets the parent rule used in testing only.
 // Should not be implemented in rules.
 func (r *basicRule) getParent() rule {
        return r.parent
 }
 
-// Return the name if not overwritten by the rule.
+// getName returns the name if not overwritten by the rule.
 // Marked as nolint as rules should override this.
 //
 //nolint:unused
@@ -75,41 +80,56 @@ func (r *basicRule) getName() string {
        return "unnamed rule"
 }
 
-// Create a new rule based on the getName of the rule requested. The rule is 
initialised with the configuration and can
-// be used directly.
+// ruleDAO returns the RuleDAO object if not overwritten by the rule.
+// Marked as nolint as rules should override this.
+//
+//nolint:unused
+func (r *basicRule) ruleDAO() *dao.RuleDAO {
+       return &dao.RuleDAO{
+               Name: r.getName(),
+       }
+}
+
+// newRule creates a new rule based on the getName of the rule requested. The 
rule is initialised with the configuration
+// and can be used directly.
+// Note that the recoveryRule should not be added to the list as it is an 
internal rule only that should not be part of
+// the configuration.
 func newRule(conf configs.PlacementRule) (rule, error) {
        // create the rule from the config
-       var newRule rule
+       var r rule
        var err error
        // create the new rule fail if the name is unknown
        switch normalise(conf.Name) {
        // rule that uses the user's name as the queue
        case types.User:
-               newRule = &userRule{}
+               r = &userRule{}
        // rule that uses a fixed queue name
        case types.Fixed:
-               newRule = &fixedRule{}
+               r = &fixedRule{}
        // rule that uses the queue provided on submit
        case types.Provided:
-               newRule = &providedRule{}
+               r = &providedRule{}
        // rule that uses a tag from the application (like namespace)
        case types.Tag:
-               newRule = &tagRule{}
+               r = &tagRule{}
+       // recovery rule must not be specified in the config
+       case types.Recovery:
+               return nil, fmt.Errorf("recovery rule cannot be part of the 
config, failing placement rule config")
        // test rule not to be used outside of testing code
        case types.Test:
-               newRule = &testRule{}
+               r = &testRule{}
        default:
                return nil, fmt.Errorf("unknown rule name specified %s, failing 
placement rule config", conf.Name)
        }
 
        // initialise the rule: do not expect the rule to log errors
-       err = newRule.initialise(conf)
+       err = r.initialise(conf)
        if err != nil {
                log.Log(log.Config).Error("Rule init failed", zap.Error(err))
                return nil, err
        }
        log.Log(log.Config).Debug("New rule created", zap.Any("ruleConf", conf))
-       return newRule, nil
+       return r, nil
 }
 
 // Normalise the rule name from the config.
diff --git a/pkg/scheduler/placement/rule_test.go 
b/pkg/scheduler/placement/rule_test.go
index f4e741c8..3df65c3d 100644
--- a/pkg/scheduler/placement/rule_test.go
+++ b/pkg/scheduler/placement/rule_test.go
@@ -52,6 +52,15 @@ func TestNewRule(t *testing.T) {
        if err != nil || nr == nil {
                t.Errorf("new normalised newRule build failed which should not, 
newRule 'nil' , err: %v, ", err)
        }
+
+       // test deny recovery rule name
+       conf = configs.PlacementRule{
+               Name: "recovery",
+       }
+       nr, err = newRule(conf)
+       if err == nil || nr != nil {
+               t.Errorf("new newRule did not fail with recovery rule name, err 
'nil' , newRule: %v, ", nr)
+       }
 }
 
 // Test for a basic test rule.
diff --git a/pkg/scheduler/placement/tag_rule.go 
b/pkg/scheduler/placement/tag_rule.go
index 95f38ea3..6bc54b42 100644
--- a/pkg/scheduler/placement/tag_rule.go
+++ b/pkg/scheduler/placement/tag_rule.go
@@ -20,6 +20,7 @@ package placement
 
 import (
        "fmt"
+       "strconv"
        "strings"
 
        "go.uber.org/zap"
@@ -28,6 +29,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/log"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
        "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 // A rule to place an application based on the a tag on the application.
@@ -43,6 +45,22 @@ func (tr *tagRule) getName() string {
        return types.Tag
 }
 
+func (tr *tagRule) ruleDAO() *dao.RuleDAO {
+       var pDAO *dao.RuleDAO
+       if tr.parent != nil {
+               pDAO = tr.parent.ruleDAO()
+       }
+       return &dao.RuleDAO{
+               Name: tr.getName(),
+               Parameters: map[string]string{
+                       "tagName": tr.tagName,
+                       "create":  strconv.FormatBool(tr.create),
+               },
+               ParentRule: pDAO,
+               Filter:     tr.filter.filterDAO(),
+       }
+}
+
 func (tr *tagRule) initialise(conf configs.PlacementRule) error {
        tr.tagName = normalise(conf.Value)
        if tr.tagName == "" {
diff --git a/pkg/scheduler/placement/tag_rule_test.go 
b/pkg/scheduler/placement/tag_rule_test.go
index 6eb8e79e..527ea736 100644
--- a/pkg/scheduler/placement/tag_rule_test.go
+++ b/pkg/scheduler/placement/tag_rule_test.go
@@ -26,6 +26,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/common"
        "github.com/apache/yunikorn-core/pkg/common/configs"
        "github.com/apache/yunikorn-core/pkg/common/security"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 func TestTagRule(t *testing.T) {
@@ -251,3 +252,40 @@ func TestTagRuleParent(t *testing.T) {
                t.Errorf("tag rule placed app in incorrect queue '%s', err %v", 
queue, err)
        }
 }
+
+func Test_tagRule_ruleDAO(t *testing.T) {
+       tests := []struct {
+               name string
+               conf configs.PlacementRule
+               want *dao.RuleDAO
+       }{
+               {
+                       "base",
+                       configs.PlacementRule{Name: "tag", Value: "namespace"},
+                       &dao.RuleDAO{Name: "tag", Parameters: 
map[string]string{"tagName": "namespace", "create": "false"}},
+               },
+               {
+                       "mixedcase",
+                       configs.PlacementRule{Name: "tag", Value: 
"MixedCaseTag"},
+                       &dao.RuleDAO{Name: "tag", Parameters: 
map[string]string{"tagName": "mixedcasetag", "create": "false"}},
+               },
+               {
+                       "parent",
+                       configs.PlacementRule{Name: "tag", Value: "one", 
Create: true, Parent: &configs.PlacementRule{Name: "test", Create: true}},
+                       &dao.RuleDAO{Name: "tag", Parameters: 
map[string]string{"tagName": "one", "create": "true"}, ParentRule: 
&dao.RuleDAO{Name: "test", Parameters: map[string]string{"create": "true"}}},
+               },
+               {
+                       "filter",
+                       configs.PlacementRule{Name: "tag", Value: "two", 
Create: true, Filter: configs.Filter{Type: filterDeny, Groups: 
[]string{"group[0-9]"}}},
+                       &dao.RuleDAO{Name: "tag", Parameters: 
map[string]string{"tagName": "two", "create": "true"}, Filter: 
&dao.FilterDAO{Type: filterDeny, GroupExp: "group[0-9]"}},
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       ur, err := newRule(tt.conf)
+                       assert.NilError(t, err, "setting up the rule failed")
+                       ruleDAO := ur.ruleDAO()
+                       assert.DeepEqual(t, tt.want, ruleDAO)
+               })
+       }
+}
diff --git a/pkg/scheduler/placement/testrule.go 
b/pkg/scheduler/placement/testrule.go
index fce27c68..f06a145e 100644
--- a/pkg/scheduler/placement/testrule.go
+++ b/pkg/scheduler/placement/testrule.go
@@ -20,10 +20,12 @@ package placement
 
 import (
        "fmt"
+       "strconv"
 
        "github.com/apache/yunikorn-core/pkg/common/configs"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
        "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 // A simple test rule to place an application based on a nil application.
@@ -36,6 +38,21 @@ func (tr *testRule) getName() string {
        return types.Test
 }
 
+func (tr *testRule) ruleDAO() *dao.RuleDAO {
+       var pDAO *dao.RuleDAO
+       if tr.parent != nil {
+               pDAO = tr.parent.ruleDAO()
+       }
+       return &dao.RuleDAO{
+               Name: types.Test,
+               Parameters: map[string]string{
+                       "create": strconv.FormatBool(tr.create),
+               },
+               ParentRule: pDAO,
+               Filter:     tr.filter.filterDAO(),
+       }
+}
+
 // Simple init for the test rule: allow everything as per a normal rule.
 func (tr *testRule) initialise(conf configs.PlacementRule) error {
        tr.create = conf.Create
diff --git a/pkg/scheduler/placement/user_rule.go 
b/pkg/scheduler/placement/user_rule.go
index 2c195e4e..1e1f914d 100644
--- a/pkg/scheduler/placement/user_rule.go
+++ b/pkg/scheduler/placement/user_rule.go
@@ -20,6 +20,7 @@ package placement
 
 import (
        "fmt"
+       "strconv"
        "strings"
 
        "go.uber.org/zap"
@@ -28,6 +29,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/log"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
        "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 // A rule to place an application based on the user name of the submitting 
user.
@@ -39,6 +41,21 @@ func (ur *userRule) getName() string {
        return types.User
 }
 
+func (ur *userRule) ruleDAO() *dao.RuleDAO {
+       var pDAO *dao.RuleDAO
+       if ur.parent != nil {
+               pDAO = ur.parent.ruleDAO()
+       }
+       return &dao.RuleDAO{
+               Name: ur.getName(),
+               Parameters: map[string]string{
+                       "create": strconv.FormatBool(ur.create),
+               },
+               ParentRule: pDAO,
+               Filter:     ur.filter.filterDAO(),
+       }
+}
+
 func (ur *userRule) initialise(conf configs.PlacementRule) error {
        ur.create = conf.Create
        ur.filter = newFilter(conf.Filter)
diff --git a/pkg/scheduler/placement/user_rule_test.go 
b/pkg/scheduler/placement/user_rule_test.go
index 9e7126e3..62b099e8 100644
--- a/pkg/scheduler/placement/user_rule_test.go
+++ b/pkg/scheduler/placement/user_rule_test.go
@@ -25,6 +25,7 @@ import (
 
        "github.com/apache/yunikorn-core/pkg/common/configs"
        "github.com/apache/yunikorn-core/pkg/common/security"
+       "github.com/apache/yunikorn-core/pkg/webservice/dao"
 )
 
 func TestUserRulePlace(t *testing.T) {
@@ -219,3 +220,35 @@ func TestUserRuleParent(t *testing.T) {
                t.Errorf("user rule placed app in incorrect queue '%s', err 
%v", queue, err)
        }
 }
+
+func Test_userRule_ruleDAO(t *testing.T) {
+       tests := []struct {
+               name string
+               conf configs.PlacementRule
+               want *dao.RuleDAO
+       }{
+               {
+                       "base",
+                       configs.PlacementRule{Name: "user"},
+                       &dao.RuleDAO{Name: "user", Parameters: 
map[string]string{"create": "false"}},
+               },
+               {
+                       "parent",
+                       configs.PlacementRule{Name: "user", Create: true, 
Parent: &configs.PlacementRule{Name: "test", Create: true}},
+                       &dao.RuleDAO{Name: "user", Parameters: 
map[string]string{"create": "true"}, ParentRule: &dao.RuleDAO{Name: "test", 
Parameters: map[string]string{"create": "true"}}},
+               },
+               {
+                       "filter",
+                       configs.PlacementRule{Name: "user", Create: true, 
Filter: configs.Filter{Type: filterDeny, Users: []string{"user"}}},
+                       &dao.RuleDAO{Name: "user", Parameters: 
map[string]string{"create": "true"}, Filter: &dao.FilterDAO{Type: filterDeny, 
UserList: []string{"user"}}},
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       ur, err := newRule(tt.conf)
+                       assert.NilError(t, err, "setting up the rule failed")
+                       ruleDAO := ur.ruleDAO()
+                       assert.DeepEqual(t, tt.want, ruleDAO)
+               })
+       }
+}
diff --git a/pkg/webservice/dao/rule_info.go b/pkg/webservice/dao/rule_info.go
new file mode 100644
index 00000000..9e852864
--- /dev/null
+++ b/pkg/webservice/dao/rule_info.go
@@ -0,0 +1,39 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package dao
+
+type RuleDAOInfo struct {
+       Partition string     `json:"partition"` // no omitempty, partition name 
should not be empty
+       Rules     []*RuleDAO `json:"rules,omitempty"`
+}
+
+type FilterDAO struct {
+       Type      string   `json:"type"` // no omitempty, type must exist
+       UserList  []string `json:"userList,omitempty"`
+       GroupList []string `json:"groupList,omitempty"`
+       UserExp   string   `json:"userExp,omitempty"`
+       GroupExp  string   `json:"groupExp,omitempty"`
+}
+
+type RuleDAO struct {
+       Name       string            `json:"name"` // no omitempty, name must 
exist
+       Parameters map[string]string `json:"parameters,omitempty"`
+       Filter     *FilterDAO        `json:"filter,omitempty"`
+       ParentRule *RuleDAO          `json:"parentRule,omitempty"`
+}
diff --git a/pkg/webservice/handlers.go b/pkg/webservice/handlers.go
index 5ccc31a5..5e37c80f 100644
--- a/pkg/webservice/handlers.go
+++ b/pkg/webservice/handlers.go
@@ -836,6 +836,25 @@ func getApplication(w http.ResponseWriter, r 
*http.Request) {
        }
 }
 
+func getPartitionRules(w http.ResponseWriter, r *http.Request) {
+       writeHeaders(w)
+       vars := httprouter.ParamsFromContext(r.Context())
+       if vars == nil {
+               buildJSONErrorResponse(w, MissingParamsName, 
http.StatusBadRequest)
+               return
+       }
+       partition := vars.ByName("partition")
+       partitionContext := 
schedulerContext.GetPartitionWithoutClusterID(partition)
+       if partitionContext == nil {
+               buildJSONErrorResponse(w, PartitionDoesNotExists, 
http.StatusNotFound)
+               return
+       }
+       rulesDao := partitionContext.GetPlacementRules()
+       if err := json.NewEncoder(w).Encode(rulesDao); err != nil {
+               buildJSONErrorResponse(w, err.Error(), 
http.StatusInternalServerError)
+       }
+}
+
 func getPartitionInfoDAO(lists map[string]*scheduler.PartitionContext) 
[]*dao.PartitionInfo {
        result := make([]*dao.PartitionInfo, 0, len(lists))
 
@@ -942,6 +961,19 @@ func getApplicationsDAO(lists 
map[string]*scheduler.PartitionContext) []*dao.App
        return result
 }
 
+func getPlacementRulesDAO(lists map[string]*scheduler.PartitionContext) 
[]*dao.RuleDAOInfo {
+       result := make([]*dao.RuleDAOInfo, 0, len(lists))
+
+       for _, partition := range lists {
+               result = append(result, &dao.RuleDAOInfo{
+                       Partition: 
common.GetPartitionNameWithoutClusterID(partition.Name),
+                       Rules:     partition.GetPlacementRules(),
+               })
+       }
+
+       return result
+}
+
 func getPartitionQueuesDAO(lists map[string]*scheduler.PartitionContext) 
[]dao.PartitionQueueDAOInfo {
        result := make([]dao.PartitionQueueDAOInfo, 0, len(lists))
 
diff --git a/pkg/webservice/handlers_test.go b/pkg/webservice/handlers_test.go
index 1b16ba3a..8bb60695 100644
--- a/pkg/webservice/handlers_test.go
+++ b/pkg/webservice/handlers_test.go
@@ -43,6 +43,7 @@ import (
        "github.com/apache/yunikorn-core/pkg/metrics/history"
        "github.com/apache/yunikorn-core/pkg/scheduler"
        "github.com/apache/yunikorn-core/pkg/scheduler/objects"
+       "github.com/apache/yunikorn-core/pkg/scheduler/placement/types"
        "github.com/apache/yunikorn-core/pkg/scheduler/policies"
        "github.com/apache/yunikorn-core/pkg/scheduler/ugm"
        "github.com/apache/yunikorn-core/pkg/webservice/dao"
@@ -50,9 +51,12 @@ import (
        "github.com/apache/yunikorn-scheduler-interface/lib/go/si"
 )
 
-const unmarshalError = "Failed to unmarshal error response from response body"
-const statusCodeError = "Incorrect Status code"
-const jsonMessageError = "JSON error message is incorrect"
+const (
+       unmarshalError   = "Failed to unmarshal error response from response 
body"
+       statusCodeError  = "Incorrect Status code"
+       jsonMessageError = "JSON error message is incorrect"
+       httpRequestError = "HTTP request creation failed"
+)
 
 const partitionNameWithoutClusterID = "default"
 const normalizedPartitionName = "[rm-123]default"
@@ -67,6 +71,7 @@ partitions:
           first: "some value with spaces"
           second: somethingElse
 `
+
 const updatedConf = `
 partitions:
   - name: default
@@ -127,6 +132,7 @@ partitions:
             name: default
             submitacl: "*"
 `
+
 const configTwoLevelQueues = `
 partitions: 
   - 
@@ -247,6 +253,27 @@ partitions:
                     cpu: "200"
 `
 
+const placementRuleConfig = `
+partitions:
+    - name: default
+      placementrules:
+      - name: user
+      - name: tag
+        value: namespace
+        create: true
+        parent:
+          name: fixed
+          value: root.namespaces
+      - name: fixed
+        value: root.default
+      queues:
+        - name: root
+          parent: true
+          submitacl: '*'
+          queues:
+            - name: default
+`
+
 const rmID = "rm-123"
 const policyGroup = "default-policy-group"
 const queueName = "root.default"
@@ -1852,7 +1879,8 @@ func TestFullStateDumpPath(t *testing.T) {
        var aggregated AggregatedStateInfo
        err = json.Unmarshal(resp.outputBytes, &aggregated)
        assert.NilError(t, err)
-       verifyStateDumpJSON(t, &aggregated)
+       // default config has only one partition
+       verifyStateDumpJSON(t, &aggregated, 1)
 }
 
 func TestSpecificUserAndGroupResourceUsage(t *testing.T) {
@@ -2523,15 +2551,18 @@ func clearUserManager() {
        userManager.ClearGroupTrackers()
 }
 
-func verifyStateDumpJSON(t *testing.T, aggregated *AggregatedStateInfo) {
+func verifyStateDumpJSON(t *testing.T, aggregated *AggregatedStateInfo, 
partitionCount int) {
        assert.Check(t, aggregated.Timestamp != 0)
-       assert.Check(t, len(aggregated.Partitions) > 0)
+       assert.Check(t, len(aggregated.Partitions) == partitionCount, 
"incorrect partition count")
        assert.Check(t, len(aggregated.Nodes) > 0)
        assert.Check(t, len(aggregated.ClusterInfo) > 0)
-       assert.Check(t, len(aggregated.Queues) > 0)
+       assert.Check(t, len(aggregated.Queues) == 1, "should only have root 
queue")
        assert.Check(t, len(aggregated.LogLevel) > 0)
-       assert.Check(t, len(aggregated.Config.SchedulerConfig.Partitions) > 0)
+       assert.Check(t, len(aggregated.Config.SchedulerConfig.Partitions) == 
partitionCount, "incorrect partition count")
        assert.Check(t, len(aggregated.Config.Extra) > 0)
+       assert.Check(t, aggregated.RMDiagnostics["empty"] != nil, "expected no 
RM registered for diagnostics")
+       assert.Check(t, len(aggregated.PlacementRules) == partitionCount, 
"incorrect partition count")
+       assert.Check(t, len(aggregated.PlacementRules[0].Rules) == 2, 
"incorrect rule count")
 }
 
 func TestCheckHealthStatusNotFound(t *testing.T) {
@@ -2598,6 +2629,58 @@ func runHealthCheckTest(t *testing.T, expected 
*dao.SchedulerHealthDAOInfo) {
        }
 }
 
+func TestGetPartitionRuleHandler(t *testing.T) {
+       setup(t, configDefault, 1)
+
+       NewWebApp(schedulerContext, nil)
+
+       // test partition not exists
+       req, err := http.NewRequest("GET", 
"/ws/v1/partition/default/placementrules", strings.NewReader(""))
+       assert.NilError(t, err, httpRequestError)
+       req = req.WithContext(context.WithValue(req.Context(), 
httprouter.ParamsKey, httprouter.Params{httprouter.Param{Key: "partition", 
Value: "notexists"}}))
+       resp := &MockResponseWriter{}
+       getPartitionRules(resp, req)
+       assertPartitionNotExists(t, resp)
+
+       // test params name missing
+       req, err = http.NewRequest("GET", 
"/ws/v1/partition/default/placementrules", strings.NewReader(""))
+       assert.NilError(t, err, httpRequestError)
+       resp = &MockResponseWriter{}
+       getPartitionRules(resp, req)
+       assertParamsMissing(t, resp)
+
+       // default config without rules defined
+       req, err = http.NewRequest("GET", 
"/ws/v1/partition/default/placementrules", strings.NewReader(""))
+       assert.NilError(t, err, httpRequestError)
+       req = req.WithContext(context.WithValue(req.Context(), 
httprouter.ParamsKey, httprouter.Params{httprouter.Param{Key: "partition", 
Value: partitionNameWithoutClusterID}}))
+       var partitionRules []*dao.RuleDAO
+       resp = &MockResponseWriter{}
+       getPartitionRules(resp, req)
+       err = json.Unmarshal(resp.outputBytes, &partitionRules)
+       assert.NilError(t, err, unmarshalError)
+       // assert content: default is provided and recovery
+       assert.Equal(t, len(partitionRules), 2)
+       assert.Equal(t, partitionRules[0].Name, types.Provided)
+       assert.Equal(t, partitionRules[1].Name, types.Recovery)
+
+       // change the config: 3 rules, expect recovery also
+       err = schedulerContext.UpdateRMSchedulerConfig(rmID, 
[]byte(placementRuleConfig))
+       assert.NilError(t, err, "Error when updating clusterInfo from config")
+       req, err = http.NewRequest("GET", 
"/ws/v1/partition/default/placementrules", strings.NewReader(""))
+       assert.NilError(t, err, httpRequestError)
+       req = req.WithContext(context.WithValue(req.Context(), 
httprouter.ParamsKey, httprouter.Params{httprouter.Param{Key: "partition", 
Value: partitionNameWithoutClusterID}}))
+       resp = &MockResponseWriter{}
+       getPartitionRules(resp, req)
+       err = json.Unmarshal(resp.outputBytes, &partitionRules)
+       assert.NilError(t, err, unmarshalError)
+       assert.Equal(t, len(partitionRules), 4)
+       assert.Equal(t, partitionRules[0].Name, types.User)
+       assert.Equal(t, partitionRules[1].Name, types.Tag)
+       assert.Equal(t, partitionRules[1].ParentRule.Name, types.Fixed)
+       assert.Equal(t, partitionRules[2].Name, types.Fixed)
+       assert.Equal(t, partitionRules[3].Name, types.Recovery)
+}
+
 type ResponseRecorderWithDeadline struct {
        *httptest.ResponseRecorder
        setWriteFails   bool
diff --git a/pkg/webservice/routes.go b/pkg/webservice/routes.go
index f3653b25..3eeef978 100644
--- a/pkg/webservice/routes.go
+++ b/pkg/webservice/routes.go
@@ -92,6 +92,12 @@ var webRoutes = routes{
                "/ws/v1/partitions",
                getPartitions,
        },
+       route{
+               "Scheduler",
+               "GET",
+               "/ws/v1/partition/:partition/placementrules",
+               getPartitionRules,
+       },
        route{
                "Scheduler",
                "GET",
diff --git a/pkg/webservice/state_dump.go b/pkg/webservice/state_dump.go
index bb3f98f3..24f769e6 100644
--- a/pkg/webservice/state_dump.go
+++ b/pkg/webservice/state_dump.go
@@ -49,6 +49,7 @@ type AggregatedStateInfo struct {
        RMDiagnostics    map[string]interface{}           
`json:"rmDiagnostics,omitempty"`
        LogLevel         string                           
`json:"logLevel,omitempty"`
        Config           *dao.ConfigDAOInfo               
`json:"config,omitempty"`
+       PlacementRules   []*dao.RuleDAOInfo               
`json:"placementRules,omitempty"`
        EventStreams     []events.EventStreamData         
`json:"eventStreams,omitempty"`
 }
 
@@ -79,6 +80,7 @@ func doStateDump(w io.Writer) error {
                RMDiagnostics:    getResourceManagerDiagnostics(),
                LogLevel:         zapConfig.Level.Level().String(),
                Config:           getClusterConfigDAO(),
+               PlacementRules:   getPlacementRulesDAO(partitionContext),
                EventStreams:     events.GetEventSystem().GetEventStreams(),
        }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscr...@yunikorn.apache.org
For additional commands, e-mail: issues-h...@yunikorn.apache.org

Reply via email to