From 1d58c51a63dda014c59b58fe8bbfffab2b736f26 Mon Sep 17 00:00:00 2001 From: dileepkumarv-avio Date: Thu, 7 Sep 2023 14:27:18 -0400 Subject: [PATCH] feat: Added linter rule to check Connection retry configuration in Mule application Added linter rule to check Connection retry configuration in Mule application --- docs/available_rules.md | 23 ++ .../AVIOGDSLRuleConfiguration.groovy | 7 + .../configuration/ConnectionRetryRule.groovy | 92 +++++++ .../ConnectionRetryRuleTest.groovy | 250 ++++++++++++++++++ 4 files changed, 372 insertions(+) create mode 100644 mule-linter-core/src/main/groovy/com/avioconsulting/mule/linter/rule/configuration/ConnectionRetryRule.groovy create mode 100644 mule-linter-core/src/test/groovy/com/avioconsulting/mule/linter/rule/configuration/ConnectionRetryRuleTest.groovy diff --git a/docs/available_rules.md b/docs/available_rules.md index 0d44dc9..e082a55 100644 --- a/docs/available_rules.md +++ b/docs/available_rules.md @@ -177,6 +177,29 @@ This argument is optional. The default array is as follows: 'localAuthorizationUrlResourceOwnerId', 'localAuthorizationUrl', 'authorizationUrl', 'passphrase'] ``` + +### CONNECTION_RETRY_CONFIG + +This rule checks that connection retry strategy is configured for all the connectors that supports retry in the mule application. +For all the connectors provided in the components list, check if reconnect configuration with property `frequency` and `count` exists at the connector, +or configured at the connector configuration in the mule application. + +The constructors for this rule are: + +```groovy +ConnectionRetryRule() +ConnectionRetryRule(List components) +``` + +*components* is a List of Maps containing `name`, `namespace` and `config-ref`. +`name` refers to the component name, `namespace` refers to the component namespace, +and `config-ref` refers to the component name for the connector configuration in global-config.xml +The most common namespaces can be referenced from the class `com.avioconsulting.mule.linter.model.Namespace`. +This argument is optional. The default list is as follows: +```groovy +[[name: 'request', namespace: Namespace.HTTP, 'config-ref': 'request-config']] +``` + ### CRON_EXPRESSION_EXTERNALIZED Cron Expressions are used in most implementation using Schedulers/Batch/Polling based integrations. diff --git a/mule-linter-core/AVIOGDSLRuleConfiguration.groovy b/mule-linter-core/AVIOGDSLRuleConfiguration.groovy index 4775c86..ac21440 100644 --- a/mule-linter-core/AVIOGDSLRuleConfiguration.groovy +++ b/mule-linter-core/AVIOGDSLRuleConfiguration.groovy @@ -27,6 +27,13 @@ mule_linter { CONFIG_PLACEHOLDER { placeholderAttributes = ['key', 'password', 'keyPassword', 'username', 'host'] } + CONNECTION_RETRY_CONFIG{ + components:[ + [name: 'request', namespace: 'http://www.mulesoft.org/schema/mule/http', 'config-ref': 'request-config'], + [name: 'publish', namespace: 'http://www.mulesoft.org/schema/mule/vm', 'config-ref': 'config '], + [name: 'publish-consume', namespace: 'http://www.mulesoft.org/schema/mule/vm', 'config-ref': 'config'] + ] + } CRON_EXPRESSION_EXTERNALIZED{} DISPLAY_NAME { components = [ diff --git a/mule-linter-core/src/main/groovy/com/avioconsulting/mule/linter/rule/configuration/ConnectionRetryRule.groovy b/mule-linter-core/src/main/groovy/com/avioconsulting/mule/linter/rule/configuration/ConnectionRetryRule.groovy new file mode 100644 index 0000000..80c07f1 --- /dev/null +++ b/mule-linter-core/src/main/groovy/com/avioconsulting/mule/linter/rule/configuration/ConnectionRetryRule.groovy @@ -0,0 +1,92 @@ +package com.avioconsulting.mule.linter.rule.configuration + +import com.avioconsulting.mule.linter.model.Application +import com.avioconsulting.mule.linter.model.Namespace +import com.avioconsulting.mule.linter.model.configuration.MuleComponent +import com.avioconsulting.mule.linter.model.rule.Param +import com.avioconsulting.mule.linter.model.rule.Rule +import com.avioconsulting.mule.linter.model.rule.RuleSeverity +import com.avioconsulting.mule.linter.model.rule.RuleViolation + +/** + * This rule checks that connection retry strategy is configured for all the connectors that supports retry in the mule application. + * For all the connectors provided in the components list, check if reconnect configuration with property `frequency` and `count` exists at the connector, + * or configured at the connector configuration in the mule application. + */ +class ConnectionRetryRule extends Rule{ + static final String RULE_ID = 'CONNECTION_RETRY_CONFIG' + static final String RULE_NAME = 'Connectors must have an reconnection strategy configured, where its supported.' + static final String RULE_VIOLATION_MESSAGE = 'Connection Retry is not configured for component: ' + + private static final String RETRY_ELEMENT = 'reconnect' + private static final String RETRY_FREQUENCY = 'frequency' + private static final String RETRY_COUNT = 'count' + private static final String DEFAULT_CONFIG_REF = 'request-config' + + /** + * components: is a List of Maps containing `name`, `namespace`, and `config-ref`. + * `config-ref` property is optional in the components list, and its defaulted to `request-config` + * The most common namespaces can be referenced from the class `com.avioconsulting.mule.linter.model.Namespace`. + * This argument is optional. The default list is as follows: + * [ + * [name: 'request', namespace: Namespace.HTTP, 'config-ref': 'request-config'] + * ] + */ + @Param("components") List components = [ + [name: 'request', namespace: Namespace.HTTP, 'config-ref': 'request-config'] + ] + + ConnectionRetryRule() { + this.ruleId = RULE_ID + this.ruleName = RULE_NAME + this.severity = RuleSeverity.MAJOR + } + + @Override + List execute(Application application) { + List violations = [] + components.each { Map component -> + String configRef = component.'config-ref' ?: DEFAULT_CONFIG_REF + application.findComponents(component.name, component.namespace).each {muleComponent -> + // check if retry strategy is configured at component, + // if retry is not configured at component, check for retry in the configuration of the connector. + if(!checkReconnectStrategy(muleComponent)) { + boolean isRetryConfigured = false + application.findComponents(configRef, component.namespace).each {configRefMuleComponent -> + if (configRefMuleComponent.getAttributeValue("name") == muleComponent.getAttributeValue("config-ref")) { + if(checkReconnectStrategy(configRefMuleComponent)) { + isRetryConfigured = true + } + } + } + if (!isRetryConfigured) { + violations.add(new RuleViolation(this, muleComponent.file.path, muleComponent.lineNumber, + RULE_VIOLATION_MESSAGE + muleComponent.componentName)) + } + } + } + } + return violations + } + /** + * This method iterates through MuleComponent and returns `true` if reconnect configuration exists for the component. + * returns boolean + */ + boolean checkReconnectStrategy(MuleComponent muleComponent){ + boolean isRetryConfigured = false + if(muleComponent.componentName == RETRY_ELEMENT + && muleComponent.hasAttributeValue(RETRY_FREQUENCY) && muleComponent.hasAttributeValue(RETRY_COUNT)){ + isRetryConfigured = true + }else if(muleComponent.children.size() > 0){ + for (MuleComponent childMuleComponent : muleComponent.children){ + if(checkReconnectStrategy(childMuleComponent)){ + isRetryConfigured = true + break + } + } + }else if (muleComponent.children.size()==0){ + isRetryConfigured = false + } + return isRetryConfigured + } +} diff --git a/mule-linter-core/src/test/groovy/com/avioconsulting/mule/linter/rule/configuration/ConnectionRetryRuleTest.groovy b/mule-linter-core/src/test/groovy/com/avioconsulting/mule/linter/rule/configuration/ConnectionRetryRuleTest.groovy new file mode 100644 index 0000000..d8b01a9 --- /dev/null +++ b/mule-linter-core/src/test/groovy/com/avioconsulting/mule/linter/rule/configuration/ConnectionRetryRuleTest.groovy @@ -0,0 +1,250 @@ +package com.avioconsulting.mule.linter.rule.configuration + +import com.avioconsulting.mule.linter.TestApplication +import com.avioconsulting.mule.linter.model.Application +import com.avioconsulting.mule.linter.model.MuleApplication +import com.avioconsulting.mule.linter.model.Namespace +import com.avioconsulting.mule.linter.model.rule.Rule +import com.avioconsulting.mule.linter.rule.configuration.ConnectionRetryRule +import spock.lang.Specification + +class ConnectionRetryRuleTest extends Specification{ + private final TestApplication testApp = new TestApplication() + private Application app + + def setup() { + testApp.initialize() + testApp.addPom() + } + + def cleanup() { + testApp.remove() + } + + def 'HTTP Request with retry success'() { + given: + testApp.addFile('src/main/mule/global-config.xml', GLOBAL_CONFIG) + testApp.buildConfigContent('implementation.xml', IMPLEMENTATION_WITH_RETRY) + app = new MuleApplication(testApp.appDir) + Rule rule = new ConnectionRetryRule() + rule.init() + + when: + def violations = rule.execute(app) + + then: + violations.size() == 0 + } + + def 'HTTP Request-config with retry configured at config-reference success'() { + given: + testApp.addFile('src/main/mule/global-config.xml', GLOBAL_CONFIG) + testApp.buildConfigContent('implementation.xml', IMPLEMENTATION_WITHOUT_RETRY) + app = new MuleApplication(testApp.appDir) + Rule rule = new ConnectionRetryRule() + rule.init() + + when: + def violations = rule.execute(app) + + then: + violations.size() == 0 + } + + def 'HTTP Request without retry failure'() { + given: + testApp.addFile('src/main/mule/global-config.xml', GLOBAL_CONFIG_WITHOUT_RETRY) + testApp.buildConfigContent('implementation.xml', IMPLEMENTATION_WITHOUT_RETRY) + app = new MuleApplication(testApp.appDir) + Rule rule = new ConnectionRetryRule() + rule.init() + + when: + def violations = rule.execute(app) + + then: + violations.size() == 1 + violations[0].message == 'Connection Retry is not configured for component: request' + } + + def 'Implementation with more components to check for retry success '() { + given: + testApp.addFile('src/main/mule/global-config.xml', GLOBAL_CONFIG) + testApp.buildConfigContent('implementation.xml', IMPLEMENTATION_WITH_RETRY) + List components = [[name: 'request', namespace: Namespace.HTTP], + [name: 'publish', namespace: Namespace.VM, 'config-ref': 'config'], + [name: 'publish-consume', namespace: Namespace.VM, 'config-ref': 'config']] + app = new MuleApplication(testApp.appDir) + Rule rule = new ConnectionRetryRule() + rule.setProperty('components',components) + rule.init() + + when: + def violations = rule.execute(app) + + then: + violations.size() == 0 + } + + def 'Implementation with more components to check for retry configuration at config-ref success '() { + given: + testApp.addFile('src/main/mule/global-config.xml', GLOBAL_CONFIG) + testApp.buildConfigContent('implementation.xml', IMPLEMENTATION_WITHOUT_RETRY) + List components = [[name: 'request', namespace: Namespace.HTTP, 'config-ref': 'request-config'], + [name: 'publish', namespace: Namespace.VM, 'config-ref': 'config'], + [name: 'publish-consume', namespace: Namespace.VM, 'config-ref': 'config']] + app = new MuleApplication(testApp.appDir) + Rule rule = new ConnectionRetryRule() + rule.setProperty('components',components) + rule.init() + + when: + def violations = rule.execute(app) + + then: + violations.size() == 0 + } + + def 'Implementation with more components to check for retry failure'() { + given: + testApp.addFile('src/main/mule/global-config.xml', GLOBAL_CONFIG_WITHOUT_RETRY) + testApp.buildConfigContent('implementation.xml', IMPLEMENTATION_WITHOUT_RETRY) + List components = [[name: 'request', namespace: Namespace.HTTP, 'config-ref': 'request-config'], + [name: 'publish', namespace: Namespace.VM, 'config-ref': 'config '], + [name: 'publish-consume', namespace: Namespace.VM, 'config-ref': 'config']] + app = new MuleApplication(testApp.appDir) + ConnectionRetryRule rule = new ConnectionRetryRule() + rule.setProperty('components',components) + rule.init() + + when: + def violations = rule.execute(app) + + then: + violations.size() == 3 + violations[0].message == 'Connection Retry is not configured for component: request' + violations[1].message == 'Connection Retry is not configured for component: publish' + violations[2].message == 'Connection Retry is not configured for component: publish-consume' + } + + private static final String GLOBAL_CONFIG = ''' + +\t +\t\t +\t +\t +\t\t +\t\t\t +\t\t\t\t +\t\t\t +\t\t +\t +\t +\t\t +\t\t\t +\t\t\t\t +\t\t\t +\t\t +\t\t +\t\t\t +\t\t +\t + +''' + private static final String GLOBAL_CONFIG_WITHOUT_RETRY = ''' + +\t +\t\t +\t +\t +\t\t +\t\t +\t +\t +\t\t +\t\t\t +\t\t +\t + +''' + private static final String IMPLEMENTATION_WITH_RETRY = ''' + +\t +\t\t +\t\t +\t\t\t +\t\t +\t\t +\t\t\t +\t\t +\t\t +\t\t\t +\t\t +\t\t +\t\t +\t\t\t +\t\t\t +\t\t +\t\t +\t\t +\t\t\t +\t\t +\t\t +\t +''' + + private static final String IMPLEMENTATION_WITHOUT_RETRY = ''' + +\t +\t\t +\t\t +\t\t\t +\t\t +\t\t +\t\t +\t\t +\t\t +\t\t\t +\t\t +\t\t +\t\t +\t\t\t +\t\t +\t\t +\t +''' +}