Skip to content

Commit 9f561ec

Browse files
committed
Add support for generating placement with raw data
Placements can now be generated by nesting a full labelSelector or clusterSelector YAML under the `placement` key, in addition to the legacy method of listing key/value pairs under `placement`. This allows users to specify multiple clusters in the values array of a placement or placementRule, and to use matchLabels instead of matchExpressions (only supported by placement). Signed-off-by: Will Kutler <[email protected]>
1 parent e32fb03 commit 9f561ec

File tree

4 files changed

+416
-49
lines changed

4 files changed

+416
-49
lines changed

internal/plugin.go

Lines changed: 134 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,33 +1529,29 @@ func (p *Plugin) createPlacement(
15291529
return
15301530
}
15311531

1532+
if len(placementConfig.ClusterSelectors) > 0 && len(placementConfig.ClusterSelector) > 0 {
1533+
return "", fmt.Errorf("cannot use both clusterSelectors and clusterSelector in placement config")
1534+
}
1535+
15321536
// Determine which selectors to use
1533-
var resolvedSelectors map[string]string
1537+
var resolvedSelectors map[string]interface{}
15341538
if len(placementConfig.ClusterSelectors) > 0 {
15351539
resolvedSelectors = placementConfig.ClusterSelectors
1540+
} else if len(placementConfig.ClusterSelector) > 0 {
1541+
resolvedSelectors = placementConfig.ClusterSelector
15361542
} else if len(placementConfig.LabelSelector) > 0 {
15371543
resolvedSelectors = placementConfig.LabelSelector
15381544
}
15391545

1540-
// Sort the keys so that the match expressions can be ordered based on the label name
1546+
// build cluster selector object
15411547
keys := make([]string, 0, len(resolvedSelectors))
15421548
for key := range resolvedSelectors {
15431549
keys = append(keys, key)
15441550
}
1545-
sort.Strings(keys)
15461551

1547-
matchExpressions := []map[string]interface{}{}
1548-
for _, label := range keys {
1549-
matchExpression := map[string]interface{}{
1550-
"key": label,
1551-
}
1552-
if resolvedSelectors[label] == "" {
1553-
matchExpression["operator"] = "Exists"
1554-
} else {
1555-
matchExpression["operator"] = "In"
1556-
matchExpression["values"] = []string{resolvedSelectors[label]}
1557-
}
1558-
matchExpressions = append(matchExpressions, matchExpression)
1552+
selectorObj, err := p.generateClusterSelector(resolvedSelectors, keys)
1553+
if err != nil {
1554+
return "", err
15591555
}
15601556

15611557
if p.usingPlR {
@@ -1567,9 +1563,7 @@ func (p *Plugin) createPlacement(
15671563
"namespace": p.PolicyDefaults.Namespace,
15681564
},
15691565
"spec": map[string]interface{}{
1570-
"clusterSelector": map[string]interface{}{
1571-
"matchExpressions": matchExpressions,
1572-
},
1566+
"clusterSelector": selectorObj,
15731567
},
15741568
}
15751569
} else {
@@ -1584,9 +1578,7 @@ func (p *Plugin) createPlacement(
15841578
"predicates": []map[string]interface{}{
15851579
{
15861580
"requiredClusterSelector": map[string]interface{}{
1587-
"labelSelector": map[string]interface{}{
1588-
"matchExpressions": matchExpressions,
1589-
},
1581+
"labelSelector": selectorObj,
15901582
},
15911583
},
15921584
},
@@ -1621,6 +1613,127 @@ func (p *Plugin) createPlacement(
16211613
return
16221614
}
16231615

1616+
func (p *Plugin) generateClusterSelector(
1617+
resolvedSelectors map[string]interface{},
1618+
keys []string,
1619+
) (map[string]interface{}, error) {
1620+
// Determine cluster selector type
1621+
var selectorType string
1622+
var usedSelectors []string
1623+
var legacy bool
1624+
1625+
for key := range resolvedSelectors {
1626+
if key == "matchExpressions" {
1627+
usedSelectors = append(usedSelectors, key)
1628+
} else if key == "matchLabels" {
1629+
usedSelectors = append(usedSelectors, key)
1630+
} else if !legacy {
1631+
usedSelectors = append(usedSelectors, "legacy")
1632+
legacy = true
1633+
}
1634+
}
1635+
1636+
// Determine cluster selector type
1637+
if len(usedSelectors) > 1 {
1638+
return nil, fmt.Errorf("too many placement selectors: %s", strings.Join(usedSelectors, ", "))
1639+
} else if len(usedSelectors) == 1 {
1640+
selectorType = usedSelectors[0]
1641+
}
1642+
1643+
if selectorType == "matchLabels" && p.usingPlR {
1644+
return nil, fmt.Errorf("placementRule does not support matchLabels")
1645+
}
1646+
1647+
// build cluster selector object
1648+
var selector interface{}
1649+
1650+
if selectorType == "matchExpressions" {
1651+
matchExpressions := []map[string]interface{}{}
1652+
// complete selector provided, so just verify it casts properly
1653+
for _, matchExpression := range resolvedSelectors[selectorType].([]interface{}) {
1654+
matchExpression, ok := matchExpression.(map[string]interface{})
1655+
if !ok {
1656+
return nil,
1657+
fmt.Errorf(
1658+
"%s selector has non-map value in list: %s",
1659+
selectorType,
1660+
fmt.Sprint(resolvedSelectors[selectorType]),
1661+
)
1662+
}
1663+
1664+
// verify matchExpressions matches desired format
1665+
_, keyExists := matchExpression["key"].(string)
1666+
_, opExists := matchExpression["operator"].(string)
1667+
1668+
if !keyExists || !opExists {
1669+
return nil,
1670+
fmt.Errorf(
1671+
"matchExpression formatted incorrectly: %s",
1672+
fmt.Sprint(matchExpression),
1673+
)
1674+
}
1675+
1676+
_, valExists := matchExpression["values"]
1677+
if valExists {
1678+
_, valFormat := matchExpression["values"].([]string)
1679+
if !valFormat {
1680+
return nil,
1681+
fmt.Errorf(
1682+
"matchExpression formatted incorrectly: %s",
1683+
fmt.Sprint(matchExpression),
1684+
)
1685+
}
1686+
}
1687+
1688+
matchExpressions = append(matchExpressions, matchExpression)
1689+
}
1690+
1691+
selector = matchExpressions
1692+
} else if selectorType == "matchLabels" {
1693+
// complete selector provided, so just verify it casts properly
1694+
matchLabels, ok := resolvedSelectors[selectorType].(map[string]interface{})
1695+
if !ok {
1696+
return nil,
1697+
fmt.Errorf(
1698+
"%s selector has non-map value: %s",
1699+
selectorType,
1700+
fmt.Sprint(resolvedSelectors[selectorType]),
1701+
)
1702+
}
1703+
selector = matchLabels
1704+
} else { // legacy cluster selection method, convert key/val to matchExpressions
1705+
// Sort the keys so that the match expressions can be ordered based on the label name
1706+
sort.Strings(keys)
1707+
matchExpressions := []map[string]interface{}{}
1708+
1709+
for _, label := range keys {
1710+
matchExpression := map[string]interface{}{
1711+
"key": label,
1712+
}
1713+
if resolvedSelectors[label] == "" {
1714+
matchExpression["operator"] = "Exists"
1715+
} else {
1716+
matchExpression["operator"] = "In"
1717+
value, ok := resolvedSelectors[label].(string)
1718+
if !ok {
1719+
return nil,
1720+
fmt.Errorf(
1721+
"legacy placement value has non-string type as value: %s",
1722+
fmt.Sprint(resolvedSelectors[label]),
1723+
)
1724+
}
1725+
matchExpression["values"] = []string{value}
1726+
}
1727+
matchExpressions = append(matchExpressions, matchExpression)
1728+
}
1729+
// legacy selector has been converted to matchExpressions
1730+
selectorType = "matchExpressions"
1731+
selector = matchExpressions
1732+
}
1733+
1734+
return map[string]interface{}{selectorType: selector}, nil
1735+
}
1736+
16241737
// createPlacementBinding creates a placement binding for the input placement, policies and policy sets by
16251738
// writing it to the policy generator's output buffer. An error is returned if the placement binding
16261739
// cannot be created.

internal/plugin_config_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ policies:
135135
assertReflectEqual(
136136
t,
137137
p.PolicyDefaults.Placement.ClusterSelectors,
138-
map[string]string{"cloud": "red hat"},
138+
map[string]interface{}{"cloud": "red hat"},
139139
)
140140
assertEqual(t, len(p.PolicyDefaults.Placement.LabelSelector), 0)
141141
assertEqual(t, p.PolicyDefaults.RemediationAction, "enforce")
@@ -162,7 +162,7 @@ policies:
162162
assertReflectEqual(
163163
t,
164164
policy1.Placement.ClusterSelectors,
165-
map[string]string{"cloud": "red hat"},
165+
map[string]interface{}{"cloud": "red hat"},
166166
)
167167
assertEqual(t, policy1.RemediationAction, "inform")
168168
assertEqual(t, policy1.Severity, "medium")
@@ -184,7 +184,7 @@ policies:
184184
assertReflectEqual(
185185
t,
186186
policy2.Placement.ClusterSelectors,
187-
map[string]string{"cloud": "weather"},
187+
map[string]interface{}{"cloud": "weather"},
188188
)
189189
assertEqual(t, policy2.RemediationAction, "enforce")
190190
assertEqual(t, policy2.Severity, "medium")
@@ -1240,7 +1240,7 @@ func TestPolicySetConfig(t *testing.T) {
12401240
PolicySetOptions: types.PolicySetOptions{
12411241
Placement: types.PlacementConfig{
12421242
Name: "policyset-placement",
1243-
ClusterSelectors: map[string]string{"my": "app"},
1243+
ClusterSelectors: map[string]interface{}{"my": "app"},
12441244
},
12451245
},
12461246
},
@@ -1306,8 +1306,8 @@ func TestPolicySetConfig(t *testing.T) {
13061306
Name: "my-policyset",
13071307
PolicySetOptions: types.PolicySetOptions{
13081308
Placement: types.PlacementConfig{
1309-
LabelSelector: map[string]string{"cloud": "red hat"},
1310-
ClusterSelectors: map[string]string{"cloud": "red hat"},
1309+
LabelSelector: map[string]interface{}{"cloud": "red hat"},
1310+
ClusterSelectors: map[string]interface{}{"cloud": "red hat"},
13111311
},
13121312
},
13131313
},
@@ -1325,7 +1325,7 @@ func TestPolicySetConfig(t *testing.T) {
13251325
PolicySetOptions: types.PolicySetOptions{
13261326
Placement: types.PlacementConfig{
13271327
PlacementPath: "../config/plc.yaml",
1328-
ClusterSelectors: map[string]string{"cloud": "red hat"},
1328+
ClusterSelectors: map[string]interface{}{"cloud": "red hat"},
13291329
},
13301330
},
13311331
},
@@ -1343,7 +1343,7 @@ func TestPolicySetConfig(t *testing.T) {
13431343
PolicySetOptions: types.PolicySetOptions{
13441344
Placement: types.PlacementConfig{
13451345
PlacementName: "plexistingname",
1346-
ClusterSelectors: map[string]string{"cloud": "red hat"},
1346+
ClusterSelectors: map[string]interface{}{"cloud": "red hat"},
13471347
},
13481348
},
13491349
},
@@ -1361,7 +1361,7 @@ func TestPolicySetConfig(t *testing.T) {
13611361
PolicySetOptions: types.PolicySetOptions{
13621362
Placement: types.PlacementConfig{
13631363
PlacementPath: "../config/plc.yaml",
1364-
LabelSelector: map[string]string{"cloud": "red hat"},
1364+
LabelSelector: map[string]interface{}{"cloud": "red hat"},
13651365
},
13661366
},
13671367
},
@@ -1379,7 +1379,7 @@ func TestPolicySetConfig(t *testing.T) {
13791379
PolicySetOptions: types.PolicySetOptions{
13801380
Placement: types.PlacementConfig{
13811381
PlacementName: "plexistingname",
1382-
LabelSelector: map[string]string{"cloud": "red hat"},
1382+
LabelSelector: map[string]interface{}{"cloud": "red hat"},
13831383
},
13841384
},
13851385
},
@@ -1397,7 +1397,7 @@ func TestPolicySetConfig(t *testing.T) {
13971397
PolicySetOptions: types.PolicySetOptions{
13981398
Placement: types.PlacementConfig{
13991399
PlacementRulePath: "../config/plc.yaml",
1400-
ClusterSelectors: map[string]string{"cloud": "red hat"},
1400+
ClusterSelectors: map[string]interface{}{"cloud": "red hat"},
14011401
},
14021402
},
14031403
},
@@ -1415,7 +1415,7 @@ func TestPolicySetConfig(t *testing.T) {
14151415
PolicySetOptions: types.PolicySetOptions{
14161416
Placement: types.PlacementConfig{
14171417
PlacementRuleName: "plrexistingname",
1418-
ClusterSelectors: map[string]string{"cloud": "red hat"},
1418+
ClusterSelectors: map[string]interface{}{"cloud": "red hat"},
14191419
},
14201420
},
14211421
},
@@ -1433,7 +1433,7 @@ func TestPolicySetConfig(t *testing.T) {
14331433
PolicySetOptions: types.PolicySetOptions{
14341434
Placement: types.PlacementConfig{
14351435
PlacementRulePath: "../config/plc.yaml",
1436-
LabelSelector: map[string]string{"cloud": "red hat"},
1436+
LabelSelector: map[string]interface{}{"cloud": "red hat"},
14371437
},
14381438
},
14391439
},
@@ -1451,7 +1451,7 @@ func TestPolicySetConfig(t *testing.T) {
14511451
PolicySetOptions: types.PolicySetOptions{
14521452
Placement: types.PlacementConfig{
14531453
PlacementRuleName: "plrexistingname",
1454-
LabelSelector: map[string]string{"cloud": "red hat"},
1454+
LabelSelector: map[string]interface{}{"cloud": "red hat"},
14551455
},
14561456
},
14571457
},
@@ -1497,14 +1497,14 @@ func TestPolicySetConfig(t *testing.T) {
14971497
name: "Placement and PlacementRule can't be mixed",
14981498
setupFunc: func(p *Plugin) {
14991499
p.Policies[0].Placement = types.PlacementConfig{
1500-
LabelSelector: map[string]string{"cloud": "red hat"},
1500+
LabelSelector: map[string]interface{}{"cloud": "red hat"},
15011501
}
15021502
p.PolicySets = []types.PolicySetConfig{
15031503
{
15041504
Name: "my-policyset",
15051505
PolicySetOptions: types.PolicySetOptions{
15061506
Placement: types.PlacementConfig{
1507-
ClusterSelectors: map[string]string{"cloud": "red hat"},
1507+
ClusterSelectors: map[string]interface{}{"cloud": "red hat"},
15081508
},
15091509
},
15101510
},

0 commit comments

Comments
 (0)