Skip to content

Commit 6bf5be9

Browse files
authored
Add support for the evaluation intervals (#39)
Relates: stolostron/backlog#20030 Signed-off-by: mprahl <[email protected]>
1 parent 15c6719 commit 6bf5be9

File tree

6 files changed

+498
-34
lines changed

6 files changed

+498
-34
lines changed

docs/policygenerator-reference.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ policyDefaults:
2525
# manifests being wrapped in the policy. If set to false, a configuration policy per manifest will
2626
# be generated. This defaults to true.
2727
consolidateManifests: true
28+
# Optional. This is how often a policy should be evaluated when in a particular compliance state.
29+
# When managed clusters have low CPU resources, the evaluation interval can be increased to
30+
# to reduce CPU usage on the Kubernetes API.
31+
evaluationInterval:
32+
# These are in the format of durations (e.g. "1h25m3s"). These can also be set to "never" to
33+
# avoid evaluating the policy after it has become a particular compliance state.
34+
compliant: 30m
35+
noncompliant: 45s
2836
# Optional. When the policy references a Kyverno policy manifest, this determines if an additional
2937
# configuration policy should be generated in order to receive policy violations in Open Cluster
3038
# Management when the Kyverno policy has been violated. This defaults to true.
@@ -85,6 +93,8 @@ policies:
8593
- path: ""
8694
# Optional. (See policy[0].complianceType for description.)
8795
complianceType: "musthave"
96+
# Optional. (See policyDefaults.evaluationInterval for description.)
97+
evaluationInterval: {}
8898
# Optional. A Kustomize patch to apply to the manifest(s) at the path. If there
8999
# are multiple manifests, the patch requires the apiVersion, kind, metadata.name,
90100
# and metadata.namespace (if applicable) fields to be set so Kustomize
@@ -116,6 +126,8 @@ policies:
116126
- "CM-2 Baseline Configuration"
117127
# Optional. (See policyDefaults.disabled for description.)
118128
disabled: false
129+
# Optional. (See policyDefaults.evaluationInterval for description.)
130+
evaluationInterval: {}
119131
# Optional. (See policyDefaults.informKyvernoPolicies for description.)
120132
informKyvernoPolicies: true
121133
# Optional. (See policyDefaults.consolidateManifests for description.)

internal/plugin.go

Lines changed: 164 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"path/filepath"
1010
"sort"
1111
"strings"
12+
"time"
1213

1314
"github.com/stolostron/policy-generator-plugin/internal/types"
1415
"gopkg.in/yaml.v3"
@@ -222,23 +223,85 @@ func getDefaultBool(config map[string]interface{}, key string) (value bool, set
222223
func getPolicyBool(
223224
config map[string]interface{}, policyIndex int, key string,
224225
) (value bool, set bool) {
226+
policy := getPolicy(config, policyIndex)
227+
if policy == nil {
228+
return false, false
229+
}
230+
231+
value, set = policy[key].(bool)
232+
233+
return
234+
}
235+
236+
// getPolicy will return a policy at the specified index in the Policy Generator configuration YAML.
237+
func getPolicy(config map[string]interface{}, policyIndex int) map[string]interface{} {
225238
policies, ok := config["policies"].([]interface{})
226239
if !ok {
227-
return false, false
240+
return nil
228241
}
229242

230243
if len(policies)-1 < policyIndex {
231-
return false, false
244+
return nil
232245
}
233246

234247
policy, ok := policies[policyIndex].(map[string]interface{})
235248
if !ok {
236-
return false, false
249+
return nil
237250
}
238251

239-
value, set = policy[key].(bool)
252+
return policy
253+
}
240254

241-
return
255+
// getEvaluationInterval will return the evaluation interval of specified policy in the Policy Generator configuration
256+
// YAML.
257+
func isEvaluationIntervalSet(config map[string]interface{}, policyIndex int, complianceType string) bool {
258+
policy := getPolicy(config, policyIndex)
259+
if policy == nil {
260+
return false
261+
}
262+
263+
evaluationInterval, ok := policy["evaluationInterval"].(map[string]interface{})
264+
if !ok {
265+
return false
266+
}
267+
268+
_, set := evaluationInterval[complianceType].(string)
269+
270+
return set
271+
}
272+
273+
// isEvaluationIntervalSetManifest will return the evaluation interval of the specified manifest of the specified policy
274+
// in the Policy Generator configuration YAML.
275+
func isEvaluationIntervalSetManifest(
276+
config map[string]interface{}, policyIndex int, manifestIndex int, complianceType string,
277+
) bool {
278+
policy := getPolicy(config, policyIndex)
279+
if policy == nil {
280+
return false
281+
}
282+
283+
manifests, ok := policy["manifests"].([]interface{})
284+
if !ok {
285+
return false
286+
}
287+
288+
if len(manifests)-1 < manifestIndex {
289+
return false
290+
}
291+
292+
manifest, ok := manifests[manifestIndex].(map[string]interface{})
293+
if !ok {
294+
return false
295+
}
296+
297+
evaluationInterval, ok := manifest["evaluationInterval"].(map[string]interface{})
298+
if !ok {
299+
return false
300+
}
301+
302+
_, set := evaluationInterval[complianceType].(string)
303+
304+
return set
242305
}
243306

244307
// applyDefaults applies any missing defaults under Policy.PlacementBindingDefaults,
@@ -333,6 +396,21 @@ func (p *Plugin) applyDefaults(unmarshaledConfig map[string]interface{}) {
333396
policy.Controls = p.PolicyDefaults.Controls
334397
}
335398

399+
// Only use the policyDefault evaluationInterval value when it's not explicitly set on the policy.
400+
if policy.EvaluationInterval.Compliant == "" {
401+
set := isEvaluationIntervalSet(unmarshaledConfig, i, "compliant")
402+
if !set {
403+
policy.EvaluationInterval.Compliant = p.PolicyDefaults.EvaluationInterval.Compliant
404+
}
405+
}
406+
407+
if policy.EvaluationInterval.NonCompliant == "" {
408+
set := isEvaluationIntervalSet(unmarshaledConfig, i, "noncompliant")
409+
if !set {
410+
policy.EvaluationInterval.NonCompliant = p.PolicyDefaults.EvaluationInterval.NonCompliant
411+
}
412+
}
413+
336414
if policy.PolicySets == nil {
337415
policy.PolicySets = p.PolicyDefaults.PolicySets
338416
}
@@ -409,9 +487,32 @@ func (p *Plugin) applyDefaults(unmarshaledConfig map[string]interface{}) {
409487
policy.Standards = p.PolicyDefaults.Standards
410488
}
411489

412-
for i := range policy.Manifests {
413-
if policy.Manifests[i].ComplianceType == "" {
414-
policy.Manifests[i].ComplianceType = policy.ComplianceType
490+
for j := range policy.Manifests {
491+
manifest := &policy.Manifests[j]
492+
493+
if manifest.ComplianceType == "" {
494+
manifest.ComplianceType = policy.ComplianceType
495+
}
496+
497+
// If the manifests are consolidated to a single ConfigurationPolicy object, don't set
498+
// the evaluation interval per manifest.
499+
if policy.ConsolidateManifests {
500+
continue
501+
}
502+
503+
// Only use the policy's evaluationInterval value when it's not explicitly set in the manifest.
504+
if manifest.EvaluationInterval.Compliant == "" {
505+
set := isEvaluationIntervalSetManifest(unmarshaledConfig, i, j, "compliant")
506+
if !set {
507+
manifest.EvaluationInterval.Compliant = policy.EvaluationInterval.Compliant
508+
}
509+
}
510+
511+
if manifest.EvaluationInterval.NonCompliant == "" {
512+
set := isEvaluationIntervalSetManifest(unmarshaledConfig, i, j, "noncompliant")
513+
if !set {
514+
manifest.EvaluationInterval.NonCompliant = policy.EvaluationInterval.NonCompliant
515+
}
415516
}
416517
}
417518

@@ -507,13 +608,33 @@ func (p *Plugin) assertValidConfig() error {
507608
p.PolicyDefaults.Namespace, policy.Name)
508609
}
509610

611+
if policy.EvaluationInterval.Compliant != "" && policy.EvaluationInterval.Compliant != "never" {
612+
_, err := time.ParseDuration(policy.EvaluationInterval.Compliant)
613+
if err != nil {
614+
return fmt.Errorf(
615+
"the policy %s has an invalid policy.evaluationInterval.compliant value: %w", policy.Name, err,
616+
)
617+
}
618+
}
619+
620+
if policy.EvaluationInterval.NonCompliant != "" && policy.EvaluationInterval.NonCompliant != "never" {
621+
_, err := time.ParseDuration(policy.EvaluationInterval.NonCompliant)
622+
if err != nil {
623+
return fmt.Errorf(
624+
"the policy %s has an invalid policy.evaluationInterval.noncompliant value: %w", policy.Name, err,
625+
)
626+
}
627+
}
628+
510629
if len(policy.Manifests) == 0 {
511630
return fmt.Errorf(
512631
"each policy must have at least one manifest, but found none in policy %s", policy.Name,
513632
)
514633
}
515634

516-
for _, manifest := range policy.Manifests {
635+
for j := range policy.Manifests {
636+
manifest := &policy.Manifests[j]
637+
517638
if manifest.Path == "" {
518639
return fmt.Errorf(
519640
"each policy manifest entry must have path set, but did not find a path in policy %s",
@@ -532,6 +653,40 @@ func (p *Plugin) assertValidConfig() error {
532653
if err != nil {
533654
return err
534655
}
656+
657+
evalInterval := &manifest.EvaluationInterval
658+
if policy.ConsolidateManifests && (evalInterval.Compliant != "" || evalInterval.NonCompliant != "") {
659+
return fmt.Errorf(
660+
"the policy %s has the evaluationInterval value set on manifest[%d] but "+
661+
"consolidateManifests is true",
662+
policy.Name,
663+
j,
664+
)
665+
}
666+
667+
if evalInterval.Compliant != "" && evalInterval.Compliant != "never" {
668+
_, err := time.ParseDuration(evalInterval.Compliant)
669+
if err != nil {
670+
return fmt.Errorf(
671+
"the policy %s has an invalid policy.evaluationInterval.manifest[%d].compliant value: %w",
672+
policy.Name,
673+
j,
674+
err,
675+
)
676+
}
677+
}
678+
679+
if evalInterval.NonCompliant != "" && evalInterval.NonCompliant != "never" {
680+
_, err := time.ParseDuration(evalInterval.NonCompliant)
681+
if err != nil {
682+
return fmt.Errorf(
683+
"the policy %s has an invalid policy.evaluationInterval.manifest[%d].noncompliant value: %w",
684+
policy.Name,
685+
j,
686+
err,
687+
)
688+
}
689+
}
535690
}
536691

537692
// Validate policy Placement settings

internal/plugin_config_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,117 @@ policies:
653653
assertEqual(t, err.Error(), expected)
654654
}
655655

656+
func TestConfigInvalidEvalInterval(t *testing.T) {
657+
t.Parallel()
658+
tmpDir := t.TempDir()
659+
createConfigMap(t, tmpDir, "configmap.yaml")
660+
661+
tests := []struct {
662+
// Individual values can't be used for compliant/noncompliant since an empty string means
663+
// to not inherit from the policy defaults.
664+
defaultEvalInterval string
665+
policyEvalInterval string
666+
manifestEvalInterval string
667+
expectedMsg string
668+
}{
669+
{
670+
`{"compliant": "not a duration"}`,
671+
"",
672+
"",
673+
`the policy policy-app has an invalid policy.evaluationInterval.compliant value: time: invalid duration ` +
674+
`"not a duration"`,
675+
},
676+
{
677+
`{"noncompliant": "not a duration"}`,
678+
"",
679+
"",
680+
`the policy policy-app has an invalid policy.evaluationInterval.noncompliant value: time: invalid ` +
681+
`duration "not a duration"`,
682+
},
683+
{
684+
"",
685+
`{"compliant": "not a duration"}`,
686+
"",
687+
`the policy policy-app has an invalid policy.evaluationInterval.compliant value: time: invalid duration ` +
688+
`"not a duration"`,
689+
},
690+
{
691+
"",
692+
`{"noncompliant": "not a duration"}`,
693+
"",
694+
`the policy policy-app has an invalid policy.evaluationInterval.noncompliant value: time: invalid ` +
695+
`duration "not a duration"`,
696+
},
697+
{
698+
"",
699+
"",
700+
`{"compliant": "not a duration"}`,
701+
`the policy policy-app has the evaluationInterval value set on manifest[0] but consolidateManifests is ` +
702+
`true`,
703+
},
704+
{
705+
"",
706+
"",
707+
`{"noncompliant": "not a duration"}`,
708+
`the policy policy-app has the evaluationInterval value set on manifest[0] but consolidateManifests is ` +
709+
`true`,
710+
},
711+
{
712+
"",
713+
`{"compliant": "10d5h1m"}`,
714+
"",
715+
`the policy policy-app has an invalid policy.evaluationInterval.compliant value: time: unknown unit "d" ` +
716+
`in duration "10d5h1m"`,
717+
},
718+
{
719+
"",
720+
`{"noncompliant": "1w2d"}`,
721+
"",
722+
`the policy policy-app has an invalid policy.evaluationInterval.noncompliant value: time: unknown unit ` +
723+
`"w" in duration "1w2d"`,
724+
},
725+
}
726+
727+
for _, test := range tests {
728+
test := test
729+
730+
t.Run(
731+
fmt.Sprintf("expected=%s", test.expectedMsg),
732+
func(t *testing.T) {
733+
t.Parallel()
734+
config := fmt.Sprintf(`
735+
apiVersion: policy.open-cluster-management.io/v1
736+
kind: PolicyGenerator
737+
metadata:
738+
name: policy-generator-name
739+
policyDefaults:
740+
namespace: my-policies
741+
evaluationInterval: %s
742+
policies:
743+
- name: policy-app
744+
evaluationInterval: %s
745+
manifests:
746+
- path: %s
747+
evaluationInterval: %s
748+
`,
749+
test.defaultEvalInterval,
750+
test.policyEvalInterval,
751+
path.Join(tmpDir, "configmap.yaml"),
752+
test.manifestEvalInterval,
753+
)
754+
755+
p := Plugin{}
756+
err := p.Config([]byte(config), tmpDir)
757+
if err == nil {
758+
t.Fatal("Expected an error but did not get one")
759+
}
760+
761+
assertEqual(t, err.Error(), test.expectedMsg)
762+
},
763+
)
764+
}
765+
}
766+
656767
func TestConfigNoManifests(t *testing.T) {
657768
t.Parallel()
658769
const config = `

0 commit comments

Comments
 (0)