diff --git a/README.md b/README.md index 23d1b45..3200773 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,13 @@ Supporting services can be tricky, and the Datadog Service Catalog can make it e **NOTE ABOUT VERSIONING:** This GitHub Action uses the [Service Catalog Definition APIs](https://docs.datadoghq.com/tracing/service_catalog/service_definition_api/#post-a-service-definition) at version 2. Datadog is always working to improve their APIs, and this Action will be updated to support the newer versions of the API as they become available. +## Supported Versions + +At this time, this GitHub Action supports the following versions of the Service Catalog schema: + +- `v2` +- `v2.1` + ## Wait, but why? Datadog already has methods for supplying this information. Why do we need another one? The answer is pretty simple: constraints. @@ -448,32 +455,44 @@ This is the key of your org. There are two reasons why we have this key: #### The `rules` fields +A quick note on versioning and backwards-compatibility: If a field is backwards-compatible, it will be marked as such in the table below. Not all fields are backwards-compatible, and if they are not then they will be skipped if the schema version is mismatched. We are intentionally using the same version numbers here that we use in the Service Catalog schema, this way there is less confusion. If you would like to make schema version a constraint, there is a field for that in the Org Rules File. + The Org Rules File is pretty light-weight, here's a breakdown of the fields: -| Field | Description | Required? | Default Value | -| --- | --- | --- | --- | -| `name` | The name of the rule. This is just for your own reference, it's not used by the Action. | `false` | `undefined` | -| `selection` | The selection criteria for the rule. This is a list of criteria which will be used to select the services which the rule applies to. The word `all` for the value of this field indicates that it is applicable to all definitions for the whole org. This field must be a list, except for the case of `all`. | `true` | No default | -| `selection[].tags` | These are the tags which you can use as selection criteria for this rule. These key-value pairs allow the Metadata Provider to choose which rules will apply. See examples below. | `false` | `{}` | -| `selection[].service-name` | This is the name of the service which you can use as selection criteria for this rule. | `false` | `undefined` | -| `selection[].team` | This is the name of the team which you can use as selection criteria for this rule. | `false` | `undefined` | -| `requirements` | These are the requirements which must be met for the rule to pass. More details for this field are below in "On Requirements." | `true` | No default | -| `requirements[].tags` | These are the tags which you can require for this rule. | `false` | `undefined` | -| `requirements[].tags.` | These are the tag values which you can require for this rule. If you only wish to validate the presence of the tag, use the value `ANY` to indicate that any value is valid. | `false` | `undefined` | -| `requirements[].tags..[]` | You may supply a list of acceptable values as a sequence. Keep in mind that outside of special values (such as `ANY`), all value checks are forced to locale-sensitive lower case. | `false` | `undefined` | -| `requirements[].links` | This structure allows you to have requirements surrounding the `links` section. | `false` | `undefined` | -| `requirements[].links.count` | Require at least this many `links` entries. If you require at least 1 link, you'd put a value here. | `false` | `undefined` | -| `requirements[].links.type` | Require at least one of the `links` entry to have a specific type. If you need more than one, please use two rules, one for each type. | `false` | `undefined` | -| `requirements[].docs` | This structure allows you to have requirements surrounding the `docs` section. | `false` | `undefined` | -| `requirements[].docs.provider` | Require at least one of the `docs` entry to have a specific provider. If you need more than one, please use two rules, one for each provider. **PLEASE NOTE**: This is check is case-sensitive. | `false` | `undefined` | -| `requirements[].docs.count` | Require at least this many `docs` entries. If you require at least 1 doc, you'd put a value here. | `false` | `undefined` | -| `requirements[].contacts` | This structure allows you to have requirements surrounding the `contacts` section. | `false` | `undefined` | -| `requirements[].contacts.count` | Require at least this many `contacts` entries. If you require at least 1 link, you'd put a value here. | `false` | `undefined` | -| `requirements[].contacts.type` | Require at least one of the `contacts` entry to have a specific type. If you need more than one, please use two rules, one for each type. | `false` | `undefined` | -| `requirements[].repos` | This structure allows you to have requirements surrounding the `repos` section. | `false` | `undefined` | -| `requirements[].repos.count` | Require at least this many `repos` entries. If you require at least 1 repo, you'd put a value here. | `false` | `undefined` | -| `requirements[].integrations` | This structure allows you to have requirements surrounding the `integrations` section. | `false` | `undefined` | -| `requirements[].integrations[(opsgenie|pagerduty)]` | With this requirement you can require either an OpsGenie or a PagerDuty integration. | `false` | `[]` | +| Field | Description | Required? | Default Value | Versions Supported | +| --- | --- | --- | --- | --- | +| `name` | The name of the rule. This is just for your own reference, it's not used by the Action. | `false` | `undefined` | all | +| `selection` | The selection criteria for the rule. This is a list of criteria which will be used to select the services which the rule applies to. The word `all` for the value of this field indicates that it is applicable to all definitions for the whole org. This field must be a list, except for the case of `all`. | `true` | No default | all | +| `selection[].schema-version` | The version of the schema which this rule applies to. This is a string which is used to determine which version of the schema to use. If this field is not present then the version will not be considered. Note that fields which are version-specific will cause an error if there is a mismatch, so you may wish to consider having versioned rules in those instances. The value of this field should match one of the supported versions above. | `false` | `undefined` | all | +| `selection[].tags` | These are the tags which you can use as selection criteria for this rule. These key-value pairs allow the Metadata Provider to choose which rules will apply. See examples below. | `false` | `{}` | all | +| `selection[].service-name` | This is the name of the service which you can use as selection criteria for this rule. | `false` | `undefined` | all | +| `selection[].application` | This is the name of the application which you can use as selection criteria for this rule. | `false` | `undefined` | `v2.1` | +| `selection[].tier` | This is the name of the tier which you can use as selection criteria for this rule. | `false` | `undefined` | `v2.1` | +| `selection[].lifecycle` | This is the name of the lifecycle which you can use as selection criteria for this rule. | `false` | `undefined` | `v2.1` | +| `selection[].team` | This is the name of the team which you can use as selection criteria for this rule. | `false` | `undefined` | all | +| `requirements` | These are the requirements which must be met for the rule to pass. More details for this field are below in "On Requirements." | `true` | No default | all | +| `requirements[].schema-version` | This is the version that the schema is constrained to. If this field is not present, this field is not constrained. The value of this field should match one of the supported versions above. | `false` | `undefined` | all | +| `requirements[].application` | This is the name of the application which you can use as a requirement for this rule. | `false` | `undefined` | `v2.1` | +| `requirements[].description` | This is a description of the service which you can use as a requirement for this rule. | `false` | `undefined` | `v2.1` | +| `requirements[].tier` | This is the name of the tier which you can use as a requirement for this rule. | `false` | `undefined` | `v2.1` | +| `requirements[].lifecycle` | This is the name of the lifecycle which you can use as a requirement for this rule. | `false` | `undefined` | `v2.1` | +| `requirements[].tags` | These are the tags which you can require for this rule. | `false` | `undefined` | all | +| `requirements[].tags.` | These are the tag values which you can require for this rule. If you only wish to validate the presence of the tag, use the value `ANY` to indicate that any value is valid. | `false` | `undefined` | all | +| `requirements[].tags..[]` | You may supply a list of acceptable values as a sequence. Keep in mind that outside of special values (such as `ANY`), all value checks are forced to locale-sensitive lower case. | `false` | `undefined` | all | +| `requirements[].links` | This structure allows you to have requirements surrounding the `links` section. | `false` | `undefined` | all | +| `requirements[].links.count` | Require at least this many `links` entries. If you require at least 1 link, you'd put a value here. | `false` | `undefined` | all | +| `requirements[].links.type` | Require at least one of the `links` entry to have a specific type. If you need more than one, please use two rules, one for each type. | `false` | `undefined` | all | +| `requirements[].links.provider` | Require at least one of the `links` entry to have a specific provider. If you need more than one, please use two rules, one for each provider. **PLEASE NOTE**: This is check is case-sensitive, and this property is version-specific. | `false` | `undefined` | `v2.1` | +| `requirements[].docs` | This structure allows you to have requirements surrounding the `docs` section. | `false` | `undefined` | `v2` | +| `requirements[].docs.provider` | Require at least one of the `docs` entry to have a specific provider. If you need more than one, please use two rules, one for each provider. **PLEASE NOTE**: This is check is case-sensitive. | `false` | `undefined` | `v2` | +| `requirements[].docs.count` | Require at least this many `docs` entries. If you require at least 1 doc, you'd put a value here. | `false` | `undefined` | `v2` | +| `requirements[].contacts` | This structure allows you to have requirements surrounding the `contacts` section. | `false` | `undefined` | all | +| `requirements[].contacts.count` | Require at least this many `contacts` entries. If you require at least 1 link, you'd put a value here. | `false` | `undefined` | all | +| `requirements[].contacts.type` | Require at least one of the `contacts` entry to have a specific type. If you need more than one, please use two rules, one for each type. | `false` | `undefined` | all | +| `requirements[].repos` | This structure allows you to have requirements surrounding the `repos` section. | `false` | `undefined` | `v2` | +| `requirements[].repos.count` | Require at least this many `repos` entries. If you require at least 1 repo, you'd put a value here. | `false` | `undefined` | `v2` | +| `requirements[].integrations` | This structure allows you to have requirements surrounding the `integrations` section. | `false` | `undefined` | all | +| `requirements[].integrations[(opsgenie|pagerduty)]` | With this requirement you can require either an OpsGenie or a PagerDuty integration. | `false` | `[]` | all | ##### On Selection @@ -507,20 +526,24 @@ The `requirements` section of the Org Rules file is where you will define the re The syntax for these requirements is as follows: ```yaml +--- + +org: "my-org" + rules: - name: "This is a test." selection: - - tags: - - isprod: "true" + tags: + isprod: "true" requirements: tags: - - data-sensitivity: - - critical - - high - - medium - - low - - public - - isprod: ANY + - data-sensitivity: + - critical + - high + - medium + - low + - public + - isprod: ANY links: count: 1 type: "runbook" @@ -535,19 +558,84 @@ rules: - pagerduty ``` -This is a maximal set of requirements, but here's what it means: +This is a maximal set of requirements for `v2`, but here's what it means: - The rule applies only to services which have the `isprod` tag, and the value of that tag is `true`. - The service is required to have a tag named `data-sensitivity`, and the value of that tag must be one of `critical`, `high`, `medium`, `low`, or `public`. - The rule requires that the service have at least one `links` entry with the type `runbook`. -- The rule requires that the service have at least one `docs` entry with the name `design` and the provider `confluence`. -- The rule requires that the service have at least one `contacts` entry with the name `oncall` and the type `email`. -- The rule requires that the service also have a second `contacts` entry with the type `slack`. -- The rule requires that the service have at least one `repos` entry with the name `primary`. +- The rule requires that the service have at least one `docs`. +- The rule requires that the service have at least one `contacts` entry with the type `email`. +- The rule requires that the service have at least one `repos`. - The rule requires that the service have at least one `integrations` entry called `pagerduty`. It's encouraged to be judicious in how these are required. Restrictions are inherently, well, restrictive. If you're not careful, you can end up with a situation where you're not able to add new services to your org because they don't meet the requirements of a rule. It's a good idea to start with a minimal set of requirements, and then add more as you go. +### Working with versions + +Because of versioning, the above schema will break with schema `v2.1`. Let's take a look at what this would look like if we wanted the same rules to work across all versions of the schema: + +```yaml +--- + +org: "my-org" + +rules: + - name: "This is a test. (v2)" + selection: + schema-version: v2 + tags: + isprod: "true" + requirements: + tags: + - data-sensitivity: + - critical + - high + - medium + - low + - public + - isprod: ANY + links: + count: 1 + type: "runbook" + docs: + count: 1 + contacts: + type: "email" + count: 2 + repos: + count: 1 + integrations: + - pagerduty + + - name: "This is a test. (v2.1)" + selection: + schema-version: v2.1 + tags: + isprod: "true" + requirements: + tags: + - data-sensitivity: + - critical + - high + - medium + - low + - public + - isprod: ANY + links: + count: 3 + type: + - "runbook" + - "repo" + - "doc" + contacts: + type: "email" + count: 2 + integrations: + - pagerduty +``` + +Since the `repos` and `docs` fields were rolled up under `links` in `v2.1`, this second rule now requires three links, at least one of type `runbook`, one `doc`, and one `repo`. The first rule still shows the older schema version, and works just the way it used to in `v2`. + ## Weird stuff - Datadog will _always_ force your tag names and values to lowercase. The use of lower-case characters in all tags is encouraged, in order to avoid inconsistencies between your definitions and Datadog's. diff --git a/__tests__/lib/fieldMappings.test.js b/__tests__/lib/fieldMappings.test.js index 7ef937a..377aecb 100644 --- a/__tests__/lib/fieldMappings.test.js +++ b/__tests__/lib/fieldMappings.test.js @@ -10,6 +10,7 @@ const { mappings, convenienceFields, schemaFields, + mapField, } = require('../../lib/fieldMappings') describe('constants', () => { @@ -24,4 +25,13 @@ describe('constants', () => { test('schemaFields', () => { expect(schemaFields).toMatchSnapshot() }) + + test('mapField for invalid field', () => { + core.setFailed.mockReset() + core.setFailed.mockClear() + mapField('v2', 'invalid-field', 'test')('foo:bar') + expect(core.setFailed).toHaveBeenCalledTimes(1) + core.setFailed.mockReset() + core.setFailed.mockClear() + }) }) \ No newline at end of file diff --git a/__tests__/lib/org-rules-determineRuleCompliance.test.js b/__tests__/lib/org-rules-determineRuleCompliance.test.js index 5883b8a..de2e8c2 100644 --- a/__tests__/lib/org-rules-determineRuleCompliance.test.js +++ b/__tests__/lib/org-rules-determineRuleCompliance.test.js @@ -1054,4 +1054,35 @@ integrations: | determineRuleCompliance(ruleDefinition.rules[0], serviceDefinition), ).toBeFalsy() }) + + test('#determineRuleCompliance() - schema-version selection v2.1', async () => { + const ruleDefinition = YAML.parse(` +--- +org: test-org +rules: + - name: "All services" + selection: + schema-version: v2.1 + requirements: + lifecycle: any + `) + core.__setInputsObject( + YAML.parse(` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +lifecycle: production + `), + ) + const serviceDefinition = await inputsToRegistryDocument() + + expect( + determineRuleCompliance(ruleDefinition.rules[0], serviceDefinition), + ).toBeTruthy() + }) }) diff --git a/__tests__/lib/org-rules-extra.test.js b/__tests__/lib/org-rules-extra.test.js new file mode 100644 index 0000000..fa5d96b --- /dev/null +++ b/__tests__/lib/org-rules-extra.test.js @@ -0,0 +1,301 @@ +// Seed these for GitHub's toolkit +const path = require('path') +process.env.GITHUB_EVENT_PATH = path.join( + __dirname, + '../data/github-context-payload.json', +) +process.env.GITHUB_REPOSITORY = + 'arcxp/datadog-service-catalog-metadata-provider' + +const YAML = require('yaml') + +// Pulling this in here activates the mocking of the github module +const github = require('@actions/github') + +// Need to use inputs for some of our parameters +const core = require('@actions/core') + +// This lets us get the inputs the way that they will actually come in. +const { + inputsToRegistryDocument, +} = require('../../lib/input-to-registry-document') +const { + applyOrgRules, + _test: { + fetchRemoteRules, + ghHandle, + determineApplicabilityOfRule, + determineRuleCompliance, + }, +} = require('../../lib/org-rules') + +describe.each([ + { + name: 'application', + orgRules: ` +--- + +org: test-org +rules: + - name: "All services" + selection: + schema-version: v2.1 + requirements: + application: any +`, + tests: [ + { + type: 'selection', + inputs: ` +--- + +schema-version: v2 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +`, + expected: false, + }, + { + type: 'selection', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +application: test-app +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +`, + expected: true, + }, + { + type: 'compliance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +application: test-app +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +`, + expected: true, + }, + { + type: 'compliance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +`, + expected: false, + }, + ], + }, + { + name: 'tier', + orgRules: ` +--- + +org: test-org +rules: + - name: "All services" + selection: + schema-version: v2.1 + requirements: + tier: + - p0 + - p1 + - p2 + - p3 +`, + tests: [ + { + type: 'compilance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +tier: p1 +`, + expected: true, + }, + { + type: 'compilance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +tier: horse +`, + expected: false, + }, + { + type: 'compilance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +`, + expected: false, + }, + ], + }, + { + name: 'lifecycle', + orgRules: ` +--- + +org: test-org +rules: + - name: "All services" + selection: + schema-version: v2.1 + requirements: + lifecycle: + - dev + - staging + - beta + - production + - retired +`, + tests: [ + { + type: 'compilance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +lifecycle: production +`, + expected: true, + }, + { + type: 'compilance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +`, + expected: false, + }, + { + type: 'compilance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +lifecycle: chicken +`, + expected: false, + }, + ], + }, + { + name: 'description', + orgRules: ` +--- + +org: test-org +rules: + - name: "All services" + selection: + schema-version: v2.1 + requirements: + description: any +`, + tests: [ + { + type: 'compilance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +description: testing +`, + expected: true, + }, + { + type: 'compilance', + inputs: ` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +`, + expected: false, + }, + ], + }, +])('determineApplicabilityOfRule() - $name', ({ name, orgRules, tests }) => { + beforeEach(() => { + console.warn = jest.fn() // Remove this for debugging details + core.setFailed.mockClear() + core.getInput.mockClear() + }) + + const ruleDefinition = YAML.parse(orgRules) + + test.each(tests)(`${name} - $type`, async ({ type, inputs, expected }) => { + core.__setInputsObject(YAML.parse(inputs)) + const serviceDefinition = await inputsToRegistryDocument() + + const func = + type === 'selection' + ? determineApplicabilityOfRule + : determineRuleCompliance + + expect(func(ruleDefinition.rules[0], serviceDefinition)).toEqual(expected) + }) +}) diff --git a/__tests__/org-rules-determineApplicabilityOfRule.test.js b/__tests__/org-rules-determineApplicabilityOfRule.test.js index 545c281..8b766c5 100644 --- a/__tests__/org-rules-determineApplicabilityOfRule.test.js +++ b/__tests__/org-rules-determineApplicabilityOfRule.test.js @@ -377,4 +377,95 @@ repo: foo ), ).toBeFalsy() }) + + test('#determineApplicabilityOfRule() - schema-version safe default v2', async () => { + const ruleDefinition = YAML.parse(` +--- +org: test-org +rules: + - name: "All services" + selection: all + requirements: + lifecycle: any + `) + core.__setInputsObject( + YAML.parse(` +--- + +schema-version: v2 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +lifecycle: production + `), + ) + const serviceDefinition = await inputsToRegistryDocument() + + expect( + determineApplicabilityOfRule(ruleDefinition.rules[0], serviceDefinition), + ).toBeTruthy() + }) + + test('#determineApplicabilityOfRule() - schema-version safe default v2.1', async () => { + const ruleDefinition = YAML.parse(` +--- +org: test-org +rules: + - name: "All services" + selection: all + requirements: + lifecycle: any + `) + core.__setInputsObject( + YAML.parse(` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +lifecycle: production + `), + ) + const serviceDefinition = await inputsToRegistryDocument() + + expect( + determineApplicabilityOfRule(ruleDefinition.rules[0], serviceDefinition), + ).toBeTruthy() + }) + + test('#determineApplicabilityOfRule() - schema-version selection v2.1', async () => { + const ruleDefinition = YAML.parse(` +--- +org: test-org +rules: + - name: "All services" + selection: + schema-version: v2.1 + requirements: + lifecycle: any + `) + core.__setInputsObject( + YAML.parse(` +--- + +schema-version: v2.1 +datadog-key: FAKE_KEY +datadog-app-key: FAKE_KEY +service-name: test1 +team: Team Name Here +email: 'team-name-here@fakeemaildomainthatdoesntexist.com' +lifecycle: production + `), + ) + const serviceDefinition = await inputsToRegistryDocument() + + expect( + determineApplicabilityOfRule(ruleDefinition.rules[0], serviceDefinition), + ).toBeTruthy() + }) }) diff --git a/lib/org-rules.js b/lib/org-rules.js index 8cdb576..a6258b7 100644 --- a/lib/org-rules.js +++ b/lib/org-rules.js @@ -188,28 +188,33 @@ const selectionForTags = (tags, serviceDefinition) => { return true } -/** - * Check selection criteria for a team. - * @param {string} teamName - The team name to check. - * @param {object} serviceDefinition - The service definition to use when checking. - * @returns {boolean} - True if the team name matches, false otherwise. - * @private - * @function - */ -const selectionForTeam = (teamName, serviceDefinition) => - teamName.toLocaleLowerCase() === serviceDefinition?.team?.toLocaleLowerCase() +const caseSensitiveFieldListMatch = + (fieldName) => (value, serviceDefinition) => { + if (value.toLocaleLowerCase() === 'all') { + !!_.get(serviceDefinition, fieldName, undefined) + } + if (!Array.isArray(value)) { + core.warning( + `Invalid value for ${fieldName}: ${value}; this should be either 'all' or an array of acceptable values.`, + ) + } + + return ( + value.filter((v) => _.get(serviceDefinition, fieldName, []).includes(v)) + .length > 0 + ) + } /** - * Check selection criteria for a service name. - * @param {string} serviceName - The service name to check. - * @param {object} serviceDefinition - The service definition to use when checking. - * @returns {boolean} - True if the service name matches, false otherwise. + * Make a function which checks for a field name in a service definition. + * @param {string} fieldName - The field name to check. + * @returns {function} - A function which checks for the field name in a service definition. * @private * @function - */ -const selectionForServiceName = (serviceName, serviceDefinition) => - serviceName.toLocaleLowerCase() === - serviceDefinition?.['dd-service']?.toLocaleLowerCase() + **/ +const caseInsensitiveFieldMatch = (fieldName) => (value, serviceDefinition) => + value.toLocaleLowerCase() === + serviceDefinition?.[fieldName]?.toLocaleLowerCase() /** * Determine if the rule applies to the service description. Since a single rule can have @@ -226,8 +231,12 @@ const determineApplicabilityOfRule = (rule, serviceDescription) => { const selectionCriteria = rule?.selection || undefined const selectionCheckers = { tags: selectionForTags, - 'service-name': selectionForServiceName, - team: selectionForTeam, + 'service-name': caseInsensitiveFieldMatch('dd-service'), + 'schema-version': caseInsensitiveFieldMatch('schema-version'), + team: caseInsensitiveFieldMatch('team'), + application: caseSensitiveFieldListMatch('application'), + tier: caseSensitiveFieldListMatch('tier'), + lifecycle: caseSensitiveFieldListMatch('lifecycle'), } const selectableFields = Object.keys(selectionCheckers) @@ -334,6 +343,32 @@ const makeComplianceCheck_valueMatchAndCount = ( } } +/** + * Create a compliance check function which checks that a field in the service definition using a value check, with support for `any`. + * @param {string} fieldName - The name of the field to check. + * @returns {function} - A compliance check function. + * @private + * @function + **/ +const makeSimpleStringFieldComplianceChecker = + (fieldName) => (requirement, serviceDefinition) => { + if (!requirement || _.isEmpty(requirement)) return true + if (!serviceDefinition || _.isEmpty(serviceDefinition)) return false + + // Support `any` + if ( + !Array.isArray(requirement) && + requirement.toLocaleLowerCase() === 'any' + ) { + return !!_.get(serviceDefinition, fieldName, undefined) + } + + const req_list = Array.isArray(requirement) ? requirement : [requirement] + const sd_value = _.get(serviceDefinition, fieldName, undefined) + + return req_list.includes(sd_value) + } + /** * Check if the service description complies with the tags rule. * @param {object} requirement - The requirement to check. @@ -443,6 +478,11 @@ const determineRuleCompliance = (rule, serviceDescription) => { tags: checkTagsCompliance, integrations: checkIntegrationsCompliance, + application: makeSimpleStringFieldComplianceChecker('application'), + description: makeSimpleStringFieldComplianceChecker('description'), + lifecycle: makeSimpleStringFieldComplianceChecker('lifecycle'), + tier: makeSimpleStringFieldComplianceChecker('tier'), + // Everything else can use these higher-order functions. links: makeComplianceCheck_valueMatchAndCount( 'type',