diff --git a/docs/resource-validation/README.md b/docs/resource-validation/README.md
index 4676e80338..d515b304b9 100644
--- a/docs/resource-validation/README.md
+++ b/docs/resource-validation/README.md
@@ -56,6 +56,43 @@ Policies can also reference rules in different rule sets as long as they are loa
To see the full specifications, check the [example rule-set](../../examples/resource-validation/rule-set.yaml).
+### Alternative format: Custom Rules
+
+Rules and policies can also be given using [datree's Custom Rules](https://hub.datree.io/custom-rules/custom-rules-overview) format as part of a rule set.
+
+```yaml
+apiVersion: v1
+policies:
+ - name: TestPolicy
+ isDefault: true
+ rules:
+ - identifier: TEST
+ messageOnFailure: This is a test rule
+customRules:
+ - identifier: TEST
+ name: This is a test rule
+ defaultMessageOnFailure: This is a test rule
+ schema:
+ required:
+ - test
+ properties:
+ test:
+ type: object
+ required:
+ - hello
+ properties:
+ hello:
+ type: string
+ enum:
+ - kyma
+```
+
+## Download policies
+
+You can download the currently enabled policies.
+
+In the user preferences, go to **Clusters > Resource Validation**. Click on **Download** in the **Enabled Policies** section.
+
## Scan the cluster
With the **CLUSTER_VALIDATION** [feature flag](../features.md) enabled, you can use these rules to scan existing resources in your cluster.
diff --git a/docs/resource-validation/assets/customize-policy-preferences.png b/docs/resource-validation/assets/customize-policy-preferences.png
index 2e12afed01..d0b924d241 100644
Binary files a/docs/resource-validation/assets/customize-policy-preferences.png and b/docs/resource-validation/assets/customize-policy-preferences.png differ
diff --git a/examples/resource-validation/custom-policies.yaml b/examples/resource-validation/custom-policies.yaml
new file mode 100644
index 0000000000..2bb51cff10
--- /dev/null
+++ b/examples/resource-validation/custom-policies.yaml
@@ -0,0 +1,28 @@
+# datree 'policy as code' format
+# Can be validated using datree command:
+# datree test --ignore-missing-schemas --no-record --policy-config examples/resource-validation/custom-policies.yaml examples/resource-validation/pod.yaml
+
+apiVersion: v1
+policies:
+ - name: TestPolicy
+ isDefault: true
+ rules:
+ - identifier: TEST
+ messageOnFailure: This is a test rule
+customRules:
+ - identifier: TEST
+ name: This is a test rule
+ defaultMessageOnFailure: This is a test rule
+ schema:
+ required:
+ - test
+ properties:
+ test:
+ type: object
+ required:
+ - hello
+ properties:
+ hello:
+ type: string
+ enum:
+ - kyma
diff --git a/public/i18n/en.yaml b/public/i18n/en.yaml
index e9674f2cd3..e3c0bdd6dd 100644
--- a/public/i18n/en.yaml
+++ b/public/i18n/en.yaml
@@ -219,6 +219,7 @@ common:
copy: Copy
create: Create
delete: Delete
+ download: Download
edit: Edit
edit-yaml: Edit YAML
generate-name: Generate
@@ -1334,6 +1335,8 @@ settings:
customize: Customize
validation-disabled: Resource validation is disabled
no-policies-found: No policies found
+ download-error: Error while downloading rule set
+ rule-set-missing: No rule set found
interaction:
title: Cluster Interaction
interface:
diff --git a/resources/resource-validation/rule-sets/kubernetes-pod-security-standards/rules.yaml b/resources/resource-validation/rule-sets/kubernetes-pod-security-standards/rules.yaml
index 20860d3200..85978fd7d9 100644
--- a/resources/resource-validation/rule-sets/kubernetes-pod-security-standards/rules.yaml
+++ b/resources/resource-validation/rule-sets/kubernetes-pod-security-standards/rules.yaml
@@ -346,7 +346,6 @@ rules:
properties:
runAsNonRoot:
enum:
- - null
- null
- true
validSpec:
@@ -472,7 +471,6 @@ rules:
properties:
type:
enum:
- - null
- null
- RuntimeDefault
- Localhost
diff --git a/src/components/Preferences/ResourceValidation/ResourceValidationSettings.tsx b/src/components/Preferences/ResourceValidation/ResourceValidationSettings.tsx
index 0a4a4718eb..aabcfb8343 100644
--- a/src/components/Preferences/ResourceValidation/ResourceValidationSettings.tsx
+++ b/src/components/Preferences/ResourceValidation/ResourceValidationSettings.tsx
@@ -1,4 +1,6 @@
import { useTranslation } from 'react-i18next';
+import jsyaml from 'js-yaml';
+import { saveAs } from 'file-saver';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Button, LayoutPanel, Switch } from 'fundamental-react';
import {
@@ -6,12 +8,17 @@ import {
validateResourcesState,
} from 'state/preferences/validateResourcesAtom';
import { validationSchemasState } from 'state/validationSchemasAtom';
-import { useMemo } from 'react';
+import { useCallback, useMemo } from 'react';
import { GenericList } from 'shared/components/GenericList/GenericList';
import './ResourceValidationSettings.scss';
import { useFeature } from 'hooks/useFeature';
-import { ValidationFeatureConfig } from 'state/validationEnabledSchemasAtom';
+import {
+ convertPoliciesForDatree,
+ usePolicySet,
+ ValidationFeatureConfig,
+} from 'state/validationEnabledSchemasAtom';
+import { useNotification } from 'shared/contexts/NotificationContext';
export default function ResourceValidationSettings() {
const { t } = useTranslation();
@@ -39,6 +46,34 @@ export default function ResourceValidationSettings() {
[validationSchemas],
);
+ const policySet = usePolicySet();
+ const notification = useNotification();
+
+ const download = useCallback(() => {
+ if (validationSchemas) {
+ try {
+ const customPolicyDefinition = convertPoliciesForDatree(
+ validationSchemas,
+ policySet,
+ );
+ const kubeconfigYaml = jsyaml.dump(customPolicyDefinition);
+ const blob = new Blob([kubeconfigYaml], {
+ type: 'application/yaml;charset=utf-8',
+ });
+ saveAs(blob, `customPolicies.yaml`);
+ } catch (e) {
+ console.error(e);
+ notification.notifyError({
+ content: t('settings.clusters.resourcesValidation.download-error'),
+ });
+ }
+ } else {
+ notification.notifyError({
+ content: t('settings.clusters.resourcesValidation.rule-set-missing'),
+ });
+ }
+ }, [validationSchemas, policySet, notification, t]);
+
const policyList = useMemo(() => {
const selectedPolicySet = selectedPolicies.reduce(
(agg, name) => agg.add(name),
@@ -182,6 +217,14 @@ export default function ResourceValidationSettings() {
{t('settings.clusters.resourcesValidation.reset')}
)}
+
>
}
searchSettings={{
diff --git a/src/state/validationEnabledSchemasAtom.ts b/src/state/validationEnabledSchemasAtom.ts
index b1e7bcdb2b..6d4cca7acd 100644
--- a/src/state/validationEnabledSchemasAtom.ts
+++ b/src/state/validationEnabledSchemasAtom.ts
@@ -9,6 +9,8 @@ import {
import {
emptyValidationSchema,
getEnabledRules,
+ getResolvedPolicy,
+ DatreeCustomRule,
ValidationSchema,
validationSchemasState,
} from './validationSchemasAtom';
@@ -22,6 +24,85 @@ export type ValidationFeatureConfig = {
};
};
+type DatreeCustomPolicies = {
+ apiVersion: string;
+ policies: {
+ name: string;
+ isDefault: boolean;
+ rules: {
+ identifier: string;
+ messageOnFailure: string;
+ }[];
+ }[];
+ customRules: DatreeCustomRule[];
+};
+
+/**
+ * The datree format differs between its own rules and the custom rules via 'policy as code'.
+ * This function converts the rules of a given rule set and selected policies into a format,
+ * which can be used in datree via the --policy-config option.
+ */
+export const convertPoliciesForDatree = (
+ validationSchemas: ValidationSchema,
+ policySet: Set,
+): DatreeCustomPolicies => {
+ const enabledSchemas = getValidationEnabledSchemas(
+ validationSchemas,
+ policySet,
+ );
+
+ const customRules = enabledSchemas.rules.map(
+ ({ uniqueName, name, schema, messageOnFailure }) => {
+ return {
+ identifier: uniqueName,
+ name: name ?? uniqueName,
+ defaultMessageOnFailure:
+ messageOnFailure ?? `Rule ${uniqueName} failed`,
+ schema,
+ };
+ },
+ );
+
+ const policies = enabledSchemas.policies.map(policy => {
+ const resolvedPolicy = getResolvedPolicy(
+ policy,
+ validationSchemas.policies,
+ );
+ const rules = resolvedPolicy.rules.map(rule => {
+ if (typeof rule === 'string') {
+ const messageOnFailure =
+ customRules.find(({ identifier }) => identifier)
+ ?.defaultMessageOnFailure || `Rule ${rule} failed`;
+ return {
+ identifier: rule,
+ messageOnFailure,
+ };
+ } else {
+ return {
+ identifier: rule.identifier,
+ messageOnFailure: rule.messageOnFailure ?? `Rule ${rule} failed`,
+ };
+ }
+ });
+
+ return {
+ name: resolvedPolicy.name,
+ isDefault: false,
+ rules,
+ };
+ });
+ if (policies.length > 0) {
+ policies[0].isDefault = true;
+ }
+
+ const customPolicyDefinition = {
+ apiVersion: 'v1',
+ policies,
+ customRules,
+ };
+ return customPolicyDefinition;
+};
+
const getEnabledPolicyNames = (
validationFeature: ValidationFeatureConfig,
validationPreferences: ExtendedValidateResources,
diff --git a/src/state/validationSchemasAtom.ts b/src/state/validationSchemasAtom.ts
index d46ce26705..c5527bd31f 100644
--- a/src/state/validationSchemasAtom.ts
+++ b/src/state/validationSchemasAtom.ts
@@ -15,6 +15,7 @@ import { FetchFn } from 'shared/hooks/BackendAPI/useFetch';
type Rule = {
uniqueName: string;
+ name?: string;
messageOnFailure?: string;
documentationUrl?: string;
category?: string;
@@ -22,9 +23,17 @@ type Rule = {
schema: JSONSchema4;
};
+export type DatreeCustomRule = {
+ identifier: string;
+ name: string;
+ defaultMessageOnFailure: string;
+ schema: JSONSchema4;
+};
+
type ValidationConfig = {
rules?: Array;
policies?: Array;
+ customRules?: Array;
};
export type ValidationSchema = {
@@ -41,6 +50,7 @@ type RuleReference =
| string
| {
identifier: string;
+ messageOnFailure?: string;
};
type ValidationPolicy = {
@@ -50,6 +60,20 @@ type ValidationPolicy = {
rules: Array;
};
+const convertCustomRules = (customRules?: DatreeCustomRule[]): Rule[] => {
+ if (!customRules) return [];
+ return customRules?.map(
+ ({ identifier, name, defaultMessageOnFailure, schema }) => {
+ return {
+ uniqueName: identifier,
+ name,
+ messageOnFailure: defaultMessageOnFailure,
+ schema,
+ };
+ },
+ );
+};
+
const fetchBaseValidationConfig = async (): Promise => {
try {
const response = await fetch(`/resource-validation/rule-set.yaml`);
@@ -104,9 +128,13 @@ const fetchValidationConfig = async (
permissionSet,
);
- const rules = [...validationConfig, ...configFromConfigMap].flatMap(
- schema => schema.rules ?? [],
- );
+ const rules = [
+ ...validationConfig,
+ ...configFromConfigMap,
+ ].flatMap(schema => [
+ ...(schema.rules ?? []),
+ ...convertCustomRules(schema.customRules),
+ ]);
const policies = [...validationConfig, ...configFromConfigMap].flatMap(
schema => schema.policies ?? [],
@@ -115,6 +143,14 @@ const fetchValidationConfig = async (
return { rules, policies };
};
+const getPolicyMap = (policies?: ValidationPolicy[]) => {
+ return (
+ policies?.reduce((agg, policy) => {
+ return agg.set(policy.name, policy);
+ }, new Map()) ?? (new Map() as Map)
+ );
+};
+
const getRuleKey = (rule: RuleReference) => {
return typeof rule === 'string' ? rule : rule.identifier;
};
@@ -152,6 +188,17 @@ const resolveRulesForPolicy = (
return [...rules, ...additionalRules];
};
+export const getResolvedPolicy = (
+ policy: ValidationPolicy,
+ allPolicies: ValidationPolicy[],
+) => {
+ const policyMap = getPolicyMap(allPolicies);
+ return {
+ ...policy,
+ rules: resolveRulesForPolicy(policy, policyMap),
+ };
+};
+
export const getEnabledRules = (
rules: Rule[],
selectedPolicies: ValidationPolicy[],
@@ -162,10 +209,7 @@ export const getEnabledRules = (
{},
) as { [key: string]: Rule };
- const policyMap =
- allPolicies?.reduce((agg, policy) => {
- return agg.set(policy.name, policy);
- }, new Map()) ?? (new Map() as Map);
+ const policyMap = getPolicyMap(allPolicies);
const enabledRulesByName = selectedPolicies.reduce((agg, policy) => {
const policyRules = resolveRulesForPolicy(policy, policyMap);
diff --git a/tests/integration/tests/namespace/test-resource-validation.spec.js b/tests/integration/tests/namespace/test-resource-validation.spec.js
index 38f4bf254a..653ee942d0 100644
--- a/tests/integration/tests/namespace/test-resource-validation.spec.js
+++ b/tests/integration/tests/namespace/test-resource-validation.spec.js
@@ -1,3 +1,6 @@
+import path from 'path';
+import jsyaml from 'js-yaml';
+
context('Test resource validation', () => {
Cypress.skipAfterFail();
@@ -123,6 +126,73 @@ context('Test resource validation', () => {
cy.contains('Close').click();
});
+ it('Download and reupload policies', () => {
+ cy.get('[aria-label="topnav-profile-btn"]').click();
+
+ cy.contains('Preferences').click();
+
+ cy.contains('Clusters').click();
+
+ cy.contains('Resource Validation').click();
+
+ cy.contains('Customize').click();
+
+ cy.contains('.policy-row', 'Default')
+ .find('.fd-switch')
+ .click();
+
+ cy.contains('.policy-row', 'TestPolicy')
+ .find('.fd-switch')
+ .click();
+
+ cy.contains('Download').click();
+
+ const downloadsFolder = Cypress.config('downloadsFolder');
+ cy.readFile(path.join(downloadsFolder, 'customPolicies.yaml'))
+ .then(downloadedYaml => {
+ cy.fixture('examples/resource-validation/custom-policies.yaml')
+ .then(customPolicies => jsyaml.load(customPolicies))
+ .should('deep.equal', jsyaml.load(downloadedYaml));
+ return cy.wrap(downloadedYaml);
+ })
+ .then(downloadedYaml => {
+ cy.mockConfigMap({
+ label: 'busola.io/resource-validation=rule-set',
+ data: downloadedYaml,
+ });
+
+ return cy.reload();
+ });
+
+ cy.contains('Upload YAML').click();
+
+ cy.fixture('examples/resource-validation/pod.yaml').then(podConfig => {
+ cy.pasteToMonaco(podConfig);
+ });
+
+ cy.contains('nginx:latest').should('be.visible');
+
+ cy.contains('Show warnings')
+ .should('be.visible')
+ .click();
+
+ cy.contains('This is a test rule').should('be.visible');
+
+ cy.contains('Cancel').click();
+
+ cy.get('[aria-label="topnav-profile-btn"]').click();
+
+ cy.contains('Preferences').click();
+
+ cy.contains('Clusters').click();
+
+ cy.contains('Resource Validation').click();
+
+ cy.contains('Reset').click();
+
+ cy.contains('Close').click();
+ });
+
it('Customize resource validation policies via feature flag', () => {
cy.setBusolaFeature('RESOURCE_VALIDATION', true, {
config: {