diff --git a/local/configs/jenkins.yaml b/local/configs/jenkins.yaml index 862dd7759..5e28ea9c6 100644 --- a/local/configs/jenkins.yaml +++ b/local/configs/jenkins.yaml @@ -103,8 +103,12 @@ unclassified: globalConfigName: username globalConfigEmail: username@example.com jobs: - - file: "/var/pipeline-library/src/test/resources/folders/it.dsl" + - file: "/var/pipeline-library/src/test/resources/folders/beats.dsl" - file: "/var/pipeline-library/src/test/resources/folders/getBuildInfoJsonFiles.dsl" + - file: "/var/pipeline-library/src/test/resources/folders/it.dsl" + - file: "/var/pipeline-library/src/test/resources/folders/timeout.dsl" + - file: "/var/pipeline-library/src/test/resources/jobs/beats/beatsStages.dsl" + - file: "/var/pipeline-library/src/test/resources/jobs/beats/beatsWhen.dsl" - file: "/var/pipeline-library/src/test/resources/jobs/cancelPreviousRunningBuilds.dsl" - file: "/var/pipeline-library/src/test/resources/jobs/cmd.dsl" - file: "/var/pipeline-library/src/test/resources/jobs/dockerLogin.dsl" diff --git a/src/co/elastic/beats/BeatsFunction.groovy b/src/co/elastic/beats/BeatsFunction.groovy new file mode 100644 index 000000000..b5de20867 --- /dev/null +++ b/src/co/elastic/beats/BeatsFunction.groovy @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package co.elastic.beats + +/** + Base class to implement specific functions for the beats 2.0 pipeline. +*/ +class BeatsFunction { + /** object to access to pipeline steps */ + public steps + + public BeatsFunction(Map args){ + this.steps = args.steps + } + + /** + This method should be overwritten by the target pipeline. + */ + protected run(Map args){ } +} diff --git a/src/test/groovy/ApmBasePipelineTest.groovy b/src/test/groovy/ApmBasePipelineTest.groovy index 791830b93..9d7261d1e 100644 --- a/src/test/groovy/ApmBasePipelineTest.groovy +++ b/src/test/groovy/ApmBasePipelineTest.groovy @@ -346,6 +346,7 @@ class ApmBasePipelineTest extends DeclarativePipelineTest { return script.call(m) }) helper.registerAllowedMethod('base64encode', [Map.class], { return "YWRtaW46YWRtaW4xMjMK" }) + helper.registerAllowedMethod('beatsWhen', [Map.class], null) helper.registerAllowedMethod('cancelPreviousRunningBuilds', [Map.class], null) helper.registerAllowedMethod('cmd', [Map.class], { m -> def script = loadScript('vars/cmd.groovy') diff --git a/src/test/groovy/BeatsStagesStepTests.groovy b/src/test/groovy/BeatsStagesStepTests.groovy new file mode 100644 index 000000000..5818702ac --- /dev/null +++ b/src/test/groovy/BeatsStagesStepTests.groovy @@ -0,0 +1,183 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import org.junit.Before +import org.junit.After +import org.junit.Test +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue +import co.elastic.mock.beats.RunCommand + +class BeatsStagesStepTests extends ApmBasePipelineTest { + String scriptName = 'vars/beatsStages.groovy' + + @Override + @Before + void setUp() throws Exception { + super.setUp() + } + + @Test + void test_with_no_data() throws Exception { + def script = loadScript(scriptName) + try { + script.call() + } catch (e) { + // NOOP + } + printCallStack() + assertTrue(assertMethodCallContainsPattern('error', 'project param is required')) + assertJobStatusFailure() + } + + @Test + void test_with_no_project() throws Exception { + def script = loadScript(scriptName) + try { + script.call(project: 'foo') + } catch (e) { + // NOOP + } + printCallStack() + assertTrue(assertMethodCallContainsPattern('error', 'content param is required')) + assertJobStatusFailure() + } + + @Test + void test_with_no_platform() throws Exception { + def script = loadScript(scriptName) + try { + script.call(project: 'foo', content: [:], function: new RunCommand(steps: this)) + } catch (e) { + // NOOP + } + printCallStack() + assertTrue(assertMethodCallContainsPattern('error', 'platform entry in the content is required')) + assertJobStatusFailure() + } + + @Test + void test_with_no_function() throws Exception { + def script = loadScript(scriptName) + try { + script.call(project: 'foo', content: [:]) + } catch (e) { + // NOOP + } + printCallStack() + assertTrue(assertMethodCallContainsPattern('error', 'function param is required')) + assertJobStatusFailure() + } + + @Test + void test_simple() throws Exception { + def script = loadScript(scriptName) + def ret = script.call(project: 'foo', content: [ + "platform" : [ "linux && ubuntu-16" ], + "stages": [ + "simple" : [ + "mage" : [ "foo" ] + ] + ] + ], function: new RunCommand(steps: this)) + printCallStack() + assertTrue(ret.size() == 1) + assertTrue(assertMethodCallContainsPattern('log', 'stage: foo-simple')) + assertJobStatusSuccess() + } + + @Test + void test_multiple() throws Exception { + def script = loadScript(scriptName) + def ret = script.call(project: 'foo', content: [ + "platform" : [ "linux && ubuntu-16" ], + "stages": [ + "simple" : [ + "make" : [ "foo" ] + ], + "multi" : [ + "mage" : [ "foo" ], + "platforms" : [ 'windows-2019', 'windows-2016' ] + ] + ] + ], function: new RunCommand(steps: this)) + printCallStack() + assertTrue(ret.size() == 3) + assertTrue(assertMethodCallContainsPattern('log', 'stage: foo-simple')) + assertTrue(assertMethodCallContainsPattern('log', 'stage: foo-multi-windows-2019')) + assertTrue(assertMethodCallContainsPattern('log', 'stage: foo-multi-windows-2016')) + assertJobStatusSuccess() + } + + @Test + void test_multiple_when_without_match() throws Exception { + def script = loadScript(scriptName) + helper.registerAllowedMethod('beatsWhen', [Map.class], {return false}) + def ret = script.call(project: 'foo', content: [ + "platform" : [ "linux && ubuntu-16" ], + "stages": [ + "simple" : [ + "make" : [ "foo" ] + ], + "multi" : [ + "make" : [ "foo" ], + "platforms" : [ 'windows-2019' ] + ], + "multi-when" : [ + "mage" : [ "foo" ], + "platforms" : [ 'windows-2016' ], + "when" : [ + "comments" : [ "/test auditbeat for windows" ] + ] + ] + ] + ], function: new RunCommand(steps: this)) + printCallStack() + assertTrue(ret.size() == 2) + assertFalse(assertMethodCallContainsPattern('log', 'stage: foo-multi-when')) + assertJobStatusSuccess() + } + + @Test + void test_multiple_when_with_match() throws Exception { + def script = loadScript(scriptName) + helper.registerAllowedMethod('beatsWhen', [Map.class], { return true }) + def ret = script.call(project: 'foo', content: [ + "platform" : [ "linux && ubuntu-16" ], + "stages": [ + "simple" : [ + "make" : [ "foo" ] + ], + "multi" : [ + "mage" : [ "foo" ], + "platforms" : [ 'windows-2019' ] + ], + "multi-when" : [ + "mage" : [ "foo" ], + "platforms" : [ 'windows-2016' ], + "when" : [ + "comments" : [ "/test auditbeat for windows" ] + ] + ] + ] + ], function: new RunCommand(steps: this)) + printCallStack() + assertTrue(ret.size() == 3) + assertTrue(assertMethodCallContainsPattern('log', 'stage: foo-multi-when')) + assertJobStatusSuccess() + } +} diff --git a/src/test/groovy/BeatsWhenStepTests.groovy b/src/test/groovy/BeatsWhenStepTests.groovy new file mode 100644 index 000000000..f09ed6e5e --- /dev/null +++ b/src/test/groovy/BeatsWhenStepTests.groovy @@ -0,0 +1,337 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import org.junit.Before +import org.junit.After +import org.junit.Test +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue +import co.elastic.mock.beats.GetProjectDependencies + +class BeatsWhenStepTests extends ApmBasePipelineTest { + String scriptName = 'vars/beatsWhen.groovy' + + @Override + @Before + void setUp() throws Exception { + super.setUp() + } + + @Test + void test_with_no_data() throws Exception { + def script = loadScript(scriptName) + try { + script.call() + } catch (e) { + // NOOP + } + printCallStack() + assertTrue(assertMethodCallContainsPattern('error', 'project param is required')) + assertJobStatusFailure() + } + + @Test + void test_with_no_project() throws Exception { + def script = loadScript(scriptName) + try { + script.call(project: 'foo') + } catch (e) { + // NOOP + } + printCallStack() + assertTrue(assertMethodCallContainsPattern('error', 'content param is required')) + assertJobStatusFailure() + } + + @Test + void test_with_description() throws Exception { + def script = loadScript(scriptName) + def ret = script.call(project: 'foo', description: 'bar', content: [:]) + printCallStack() + assertFalse(ret) + assertTrue(assertMethodCallContainsPattern('writeFile', 'Stages for `foo bar`')) + } + + @Test + void test_whenBranches_and_no_environment_variable() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenBranches() + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenBranches_and_environment_variable_but_no_data() throws Exception { + def script = loadScript(scriptName) + env.BRANCH_NAME = 'branch' + def ret = script.whenBranches(content: [:]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenBranches_and_environment_variable_with_data() throws Exception { + def script = loadScript(scriptName) + env.BRANCH_NAME = 'branch' + def ret = script.whenBranches(content: [ branches: true]) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenChangeset_and_no_data() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenChangeset() + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenChangeset_and_content() throws Exception { + def script = loadScript(scriptName) + def changeset = 'Jenkinsfile' + helper.registerAllowedMethod('readFile', [String.class], { return changeset }) + def ret = script.whenChangeset(content: [ changeset: ['^.ci']]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenChangeset_and_content_with_match() throws Exception { + def script = loadScript(scriptName) + def changeset = 'Jenkinsfile' + helper.registerAllowedMethod('readFile', [String.class], { return changeset }) + def ret = script.whenChangeset(content: [ changeset: ['^Jenkinsfile']]) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenChangeset_content_and_macro() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenChangeset(content: [ changeset: ['^.ci', '@oss']], + changeset: [ oss: [ '^oss'] ]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenChangeset_content_and_macro_with_match() throws Exception { + def script = loadScript(scriptName) + def changeset = 'oss' + helper.registerAllowedMethod('readFile', [String.class], { return changeset }) + def ret = script.whenChangeset(content: [ changeset: ['^.ci', '@oss']], + changeset: [ oss: [ '^oss'] ]) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenChangeset_content_and_macro_without_match() throws Exception { + def script = loadScript(scriptName) + def changeset = 'oss' + helper.registerAllowedMethod('readFile', [String.class], { return changeset }) + def ret = script.whenChangeset(content: [ changeset: ['^.ci', '@osss']], + changeset: [ oss: [ '^oss'] ]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenChangeset_content_and_function_with_match() throws Exception { + def script = loadScript(scriptName) + def changeset = 'projectA/Jenkinsfile' + helper.registerAllowedMethod('readFile', [String.class], { return changeset }) + def ret = script.whenChangeset(content: [ changeset: ['^Jenkinsfile']], + changesetFunction: new GetProjectDependencies()) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenChangeset_content_with_project_dependency_and_function_with_match() throws Exception { + def script = loadScript(scriptName) + def changeset = 'projectA/Jenkinsfile' + helper.registerAllowedMethod('readFile', [String.class], { return changeset }) + def ret = script.whenChangeset(content: [ changeset: ['#generator/common/beatgen']], + changesetFunction: new GetProjectDependencies()) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenChangeset_content_and_function_without_match() throws Exception { + def script = loadScript(scriptName) + def changeset = 'foo/Jenkinsfile' + helper.registerAllowedMethod('readFile', [String.class], { return changeset }) + def ret = script.whenChangeset(content: [ changeset: ['^Jenkinsfile']], + changesetFunction: new GetProjectDependencies()) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenComments_and_no_environment_variable() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenComments() + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenComments_and_environment_variable_but_no_data() throws Exception { + def script = loadScript(scriptName) + env.GITHUB_COMMENT = 'branch' + def ret = script.whenComments(content: [:]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenComments_and_environment_variable_with_match() throws Exception { + def script = loadScript(scriptName) + env.GITHUB_COMMENT = '/test foo' + def ret = script.whenComments(content: [ comments: ['/test foo']]) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenComments_and_environment_variable_without_match() throws Exception { + def script = loadScript(scriptName) + env.GITHUB_COMMENT = '/test foo' + def ret = script.whenComments(content: [ comments: ['/run bla', '/test bar']]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenEnabled_without_data() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenEnabled() + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenEnabled_with_data() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenEnabled(content: [:]) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenEnabled_with_disabled() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenEnabled(content: [ disabled: true]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenEnabled_with_no_disabled() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenEnabled(content: [ disabled: false]) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenLabels_and_no_data() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenLabels(content: [:]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenLabels_with_match() throws Exception { + def script = loadScript(scriptName) + helper.registerAllowedMethod('matchesPrLabel', [Map.class], { true }) + def ret = script.whenLabels(content: [ labels: ['foo']]) + printCallStack() + assertTrue(ret) + } + + @Test + void test_whenLabels_without_match() throws Exception { + def script = loadScript(scriptName) + helper.registerAllowedMethod('matchesPrLabel', [Map.class], { false }) + def ret = script.whenLabels(content: [ labels: ['foo']]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenParameters_and_no_params() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenParameters() + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenParameters_and_params_without_match() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenParameters(content: [ parameters : [ 'foo', 'bar']]) + printCallStack() + assertFalse(ret) + } + + void test_whenParameters_and_params_with_match() throws Exception { + def script = loadScript(scriptName) + params.bar = true + def ret = script.whenParameters(content: [ parameters : [ 'foo', 'bar']]) + printCallStack() + assertTrue(ret) + } + + void test_whenParameters_and_params_with_match_but_disabled() throws Exception { + def script = loadScript(scriptName) + params.bar = false + def ret = script.whenParameters(content: [ parameters : [ 'foo', 'bar']]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenTags_and_no_environment_variable() throws Exception { + def script = loadScript(scriptName) + def ret = script.whenTags() + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenTags_and_environment_variable_but_no_data() throws Exception { + def script = loadScript(scriptName) + env.TAG_NAME = 'tag' + def ret = script.whenTags(content: [:]) + printCallStack() + assertFalse(ret) + } + + @Test + void test_whenTags_and_environment_variable_with_data() throws Exception { + def script = loadScript(scriptName) + env.TAG_NAME = 'tag' + def ret = script.whenTags(content: [ tags: true]) + printCallStack() + assertTrue(ret) + } +} diff --git a/src/test/groovy/co/elastic/mock/beats/GetProjectDependencies.groovy b/src/test/groovy/co/elastic/mock/beats/GetProjectDependencies.groovy new file mode 100644 index 000000000..cd87507e7 --- /dev/null +++ b/src/test/groovy/co/elastic/mock/beats/GetProjectDependencies.groovy @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package co.elastic.mock.beats + +/** + * Mock class for the Beats 2.0 beatsWhen step + */ +class GetProjectDependencies extends co.elastic.beats.BeatsFunction { + public GetProjectDependencies(Map args = [:]){ + super(args) + } + public run(Map args = [:]){ + return [ '^projectA/.*', '^projectB' ] + } +} diff --git a/src/test/groovy/co/elastic/mock/beats/RunCommand.groovy b/src/test/groovy/co/elastic/mock/beats/RunCommand.groovy new file mode 100644 index 000000000..a758a0e09 --- /dev/null +++ b/src/test/groovy/co/elastic/mock/beats/RunCommand.groovy @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package co.elastic.mock.beats + +/** + * Mock class for the Beats 2.0 beatsStages step + */ +class RunCommand extends co.elastic.beats.BeatsFunction { + public RunCommand(Map args = [:]){ + super(args) + } + public run(Map args = [:]) { + steps.echo "-------------${args.label} ---- ${args.context}" + } +} diff --git a/src/test/resources/folders/beats.dsl b/src/test/resources/folders/beats.dsl new file mode 100644 index 000000000..94a2c6d0f --- /dev/null +++ b/src/test/resources/folders/beats.dsl @@ -0,0 +1,4 @@ +folder('it/beats') { + displayName('beats') + description('beats ITs for the APM shared library') +} diff --git a/src/test/resources/jobs/beats/beatsStages.dsl b/src/test/resources/jobs/beats/beatsStages.dsl new file mode 100644 index 000000000..952382d49 --- /dev/null +++ b/src/test/resources/jobs/beats/beatsStages.dsl @@ -0,0 +1,164 @@ +NAME = 'it/beats/beatsStages' +DSL = '''pipeline { + agent { label 'linux && immutable' } + environment { + PIPELINE_LOG_LEVEL = 'DEBUG' + } + parameters { + booleanParam(name: 'macos', defaultValue: 'true', description: '') + } + stages { + stage('prepare') { + steps { + deleteDir() + + writeYaml(file: 'simple.yaml', data: readYaml(text: """ +platform: "linux && ubuntu-16" +stages: + simple: + mage: + - "mage build test" +""")) + + writeYaml(file: 'two.yaml', data: readYaml(text: """ +platform: "linux && ubuntu-16" +stages: + one: + mage: + - "mage build test" + two: + make: + - "make -C auditbeat crosscompile" +""")) + + writeYaml(file: 'platforms.yaml', data: readYaml(text: """ +platform: "linux && ubuntu-16" +stages: + windows: + mage: + - "mage build unitTest" + platforms: + - "windows-2019" + - "windows-2016" +""")) + + writeYaml(file: 'when.yaml', data: readYaml(text: """ +platform: "linux && ubuntu-16" +stages: + windows: + mage: + - "mage build unitTest" + platforms: + - "windows-2019" + - "windows-2016" + when: + comments: + - "/test auditbeat for windows" + parameters: + - "windows" +""")) + } + } + stage('simple') { + steps { + script { + def ret = beatsStages(project: 'test', content: readYaml(file: 'simple.yaml'), function: new RunCommand(steps: this)) + whenFalse(ret.size() == 1) { + error 'Assert failed. There should be just one entry.' + } + ret.each { k,v -> + whenFalse(k.equals('test-simple')) { + error 'Assert failed. Name of the stage does not match.' + } + } + parallel(ret) + } + } + } + stage('two') { + steps { + script { + def ret = beatsStages(project: 'test', content: readYaml(file: 'two.yaml'), function: new RunCommand(steps: this)) + whenFalse(ret.size() == 2) { + error 'Assert failed. There should be just one entry.' + } + ret.each { k,v -> + whenFalse(k.equals('test-one') || k.equals('test-two')) { + error 'Assert failed. Name of the stage does not match.' + } + } + parallel(ret) + } + } + } + stage('platforms') { + steps { + script { + def ret = beatsStages(project: 'test', content: readYaml(file: 'platforms.yaml'), function: new RunCommand(steps: this)) + whenFalse(ret.size() == 2) { + error 'Assert failed. There should be just one entry.' + } + ret.each { k,v -> + whenFalse(k.equals('test-windows-windows-2016') || k.equals('test-windows-windows-2019')) { + error 'Assert failed. Name of the stage does not match.' + } + } + parallel(ret) + } + } + } + stage('when-with-comment-match') { + environment { + GITHUB_COMMENT = '/test auditbeat for windows' + } + steps { + script { + def ret = beatsStages(project: 'test', content: readYaml(file: 'when.yaml'), function: new RunCommand(steps: this)) + whenFalse(ret.size() == 2) { + error 'Assert failed. There should be just 2 entries.' + } + parallel(ret) + } + } + } + stage('when-without-comment-match') { + environment { + GITHUB_COMMENT = '/foo' + } + steps { + script { + def ret = beatsStages(project: 'test', content: readYaml(file: 'when.yaml'), function: new RunCommand(steps: this)) + whenFalse(ret.size() == 0) { + error 'Assert failed. There should be just 0 entries.' + } + parallel(ret) + } + } + } + } +} + +class RunCommand extends co.elastic.beats.BeatsFunction { + public RunCommand(Map args = [:]){ + super(args) + } + public run(Map args = [:]){ + if (args?.content?.mage) { + steps.dir(args.project) { + steps.echo "mage ${args.label}" + } + } + if (args?.content?.make) { + steps.echo "make ${args.label}" + } + } +} +''' + +pipelineJob(NAME) { + definition { + cps { + script(DSL.stripIndent()) + } + } +} diff --git a/src/test/resources/jobs/beats/beatsWhen.dsl b/src/test/resources/jobs/beats/beatsWhen.dsl new file mode 100644 index 000000000..09c6940c6 --- /dev/null +++ b/src/test/resources/jobs/beats/beatsWhen.dsl @@ -0,0 +1,88 @@ +NAME = 'it/beats/beatsWhen' +DSL = '''pipeline { + agent { label 'linux && immutable' } + environment { + PIPELINE_LOG_LEVEL = 'DEBUG' + } + parameters { + booleanParam(name: 'auditbeat', defaultValue: 'true', description: '') + } + stages { + stage('prepare') { + steps { + deleteDir() + + writeYaml(file: 'branches.yaml', data: readYaml(text: """ +when: + branches: true +""").when) + + writeYaml(file: 'comments.yaml', data: readYaml(text: """ +when: + comments: + - "/test auditbeat" + - "foo" +""").when) + + writeYaml(file: 'labels.yaml', data: readYaml(text: """ +when: + labels: + - "auditbeat" + - "foo" +""").when) + + writeYaml(file: 'parameters.yaml', data: readYaml(text: """ +when: + parameters: + - "auditbeat" + - "foo" +""").when) + + writeYaml(file: 'tags.yaml', data: readYaml(text: """ +when: + tags: true +""").when) + } + } + stage('branches') { + environment { BRANCH_NAME = 'foo' } + steps { verify('branches.yaml') } + } + stage('comment') { + environment { GITHUB_COMMENT = 'foo' } + steps { verify('comments.yaml') } + } + stage('labels') { + // labels work only for MBPs + steps { verifyFalse('labels.yaml') } + } + stage('parameters') { + steps { verify('parameters.yaml') } + } + stage('tags') { + environment { TAG_NAME = 'foo' } + steps { verify('tags.yaml') } + } + } +} + +def verify(fileName) { + whenFalse(beatsWhen(project: 'test', content: readYaml(file: fileName))) { + error 'Assert failed' + } +} + +def verifyFalse(fileName) { + whenTrue(beatsWhen(project: 'test', content: readYaml(file: fileName))) { + error 'Assert failed' + } +} +''' + +pipelineJob(NAME) { + definition { + cps { + script(DSL.stripIndent()) + } + } +} diff --git a/vars/README.md b/vars/README.md index af486a394..e15ab970b 100644 --- a/vars/README.md +++ b/vars/README.md @@ -39,6 +39,56 @@ Encode a text to base64 base64encode(text: "text to encode", encoding: "UTF-8") ``` +## beatsStages +

+ Given the YAML definition then it creates all the stages + + The list of step's params and the related default values are: +

+

+ +
+    script {
+        def mapParallelTasks = [:]
+        beatsStages(project: 'auditbeat', content: readYaml(file: 'auditbeat/Jenkinsfile.yml'), function: this.&myFunction)
+        parallel(mapParallelTasks)
+    }
+
+    def myFunction(Map args = [:]) {
+        ...
+    }
+
+ +## beatsWhen +

+ Given the YAML definition and the changeset global macros + then it verifies if the project or stage should be enabled. + + The list of step's params and the related default values are: +

+

+ +
+    whenTrue(beatsWhen(project: 'auditbeat', changesetFunction: this.&getProjectDependencies
+                       content: readYaml(file: 'auditbeat/Jenkinsfile.yml')))
+        ...
+    }
+
+    def getProjectDependencies(Map args = [:]) {
+        ...
+    }
+
+ ## build Override the `build` step to highlight in BO the URL to the downstream job. diff --git a/vars/beatsStages.groovy b/vars/beatsStages.groovy new file mode 100644 index 000000000..e901fd54c --- /dev/null +++ b/vars/beatsStages.groovy @@ -0,0 +1,71 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/** +* Given the YAML definition then it creates all the stages +*/ +Map call(Map args = [:]){ + def project = args.containsKey('project') ? args.project : error('beatsStages: project param is required') + def content = args.containsKey('content') ? args.content : error('beatsStages: content param is required') + def function = args.containsKey('function') ? args.function : error('beatsStages: function param is required') + def defaultNode = content.containsKey('platform') ? content.platform : error('beatsStages: platform entry in the content is required.') + + def mapOfStages = [:] + + content?.stages?.each { stageName, value -> + def tempMapOfStages = [:] + if (value.containsKey('when')) { + if (beatsWhen(project: project, content: value.when, description: stageName)) { + tempMapOfStages = generateStages(content: value, project: project, stageName: stageName, defaultNode: defaultNode, function: function) + } + } else { + tempMapOfStages = generateStages(content: value, project: project, stageName: stageName, defaultNode: defaultNode, function: function) + } + tempMapOfStages.each { k,v -> mapOfStages["${k}"] = v } + } + + return mapOfStages +} + +private generateStages(Map args = [:]) { + def content = args.content + def project = args.project + def stageName = args.stageName + def defaultNode = args.defaultNode + def function = args.function + + def mapOfStages = [:] + if (content.containsKey('platforms')) { + content.platforms.each { platform -> + def id = "${project}-${stageName}-${platform}" + log(level: 'DEBUG', text: "stage: ${id}") + mapOfStages[id] = generateStage(context: id, project: project, label: platform, content: content, function: function, id: id) + } + } else { + def id = "${project}-${stageName}" + log(level: 'DEBUG', text: "stage: ${id}") + mapOfStages["${id}"] = generateStage(context: id, project: project, label: defaultNode, content: content, function: function, id: id) + } + return mapOfStages +} + +private generateStage(Map args = [:]) { + def function = args.function + return { + function.run(args) + } +} diff --git a/vars/beatsStages.txt b/vars/beatsStages.txt new file mode 100644 index 000000000..2cd13032d --- /dev/null +++ b/vars/beatsStages.txt @@ -0,0 +1,22 @@ +

+ Given the YAML definition then it creates all the stages + + The list of step's params and the related default values are: +

+

+ +
+    script {
+        def mapParallelTasks = [:]
+        beatsStages(project: 'auditbeat', content: readYaml(file: 'auditbeat/Jenkinsfile.yml'), function: this.&myFunction)
+        parallel(mapParallelTasks)
+    }
+
+    def myFunction(Map args = [:]) {
+        ...
+    }
+
diff --git a/vars/beatsWhen.groovy b/vars/beatsWhen.groovy new file mode 100644 index 000000000..c32357500 --- /dev/null +++ b/vars/beatsWhen.groovy @@ -0,0 +1,180 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/** +* Given the YAML definition and the changeset global macros +* then it verifies if the project or stage should be enabled. +*/ +Boolean call(Map args = [:]){ + def project = args.containsKey('project') ? args.project : error('beatsWhen: project param is required') + def content = args.containsKey('content') ? args.content : error('beatsWhen: content param is required') + def description = args.get('description', '') + def ret = false + + markdownReason(project: project, reason: "## Build reasons for `${project} ${description}`") + if (whenEnabled(args)) { + markdownReason(project: project, reason: "
Expand to view the reasons

\n") + if (whenBranches(args)) { ret = true } + if (whenChangeset(args)) { ret = true } + if (whenComments(args)) { ret = true } + if (whenLabels(args)) { ret = true } + if (whenParameters(args)) { ret = true } + if (whenTags(args)) { ret = true } + markdownReason(project: project, reason: "

") + } + markdownReason(project: project, reason: "#### Stages for `${project} ${description}` have been ${ret ? '✅ enabled' : '❕disabled'}\n") + + return ret +} + +private Boolean whenBranches(Map args = [:]) { + if (env.BRANCH_NAME?.trim() && args.content?.get('branches')) { + markdownReason(project: args.project, reason: '* ✅ Branch is enabled .') + return true + } + markdownReason(project: args.project, reason: '* ❗Branch is `disabled`.') + return false +} + +private Boolean whenChangeset(Map args = [:]) { + if (args.content?.get('changeset')) { + // Gather macro changeset entries + def macro = [:] + args?.changeset?.each { k,v -> + macro[k] = v + } + + // Create list of changeset patterns to be searched. + def patterns = [] + args.content.changeset.each { + if (it.startsWith('@')){ + def search = it.replaceAll('@', '') + macro[search].each { macroEntry -> + patterns << macroEntry + } + } else { + patterns << it + } + } + + // If function then calculate the project dependencies on the fly. + if (args.get('changesetFunction')) { + def changesetFunction = args.changesetFunction + calculatedPatterns = changesetFunction.run(args) + patterns.addAll(calculatedPatterns) + + // Search for some other project dependencies that are explicitly + // sett with the pattern # + args.content.changeset.findAll { it.startsWith('#') }.each { + Map newArgs = args + newArgs.project = it.replaceAll('#', '') + calculatedPatterns = changesetFunction.run(args) + patterns.addAll(calculatedPatterns) + } + } + + // TODO: to be refactored with isGitRegionMatch.isPartialPatternMatch() + + // Gather the diff between the target branch and the current commit. + def gitDiffFile = 'git-diff.txt' + def from = env.CHANGE_TARGET?.trim() ? "origin/${env.CHANGE_TARGET}" : env.GIT_PREVIOUS_COMMIT + sh(script: "git diff --name-only ${from}...${env.GIT_BASE_COMMIT} > ${gitDiffFile}", returnStdout: true) + + // Search for any pattern that matches that particular + def fileContent = readFile(gitDiffFile) + match = patterns?.find { pattern -> + fileContent?.split('\n').any { line -> line ==~ pattern } + } + if (match) { + markdownReason(project: args.project, reason: "* ✅ Changeset is `enabled` and matches with the pattern `${match}`.") + return true + } else { + markdownReason(project: args.project, reason: "* ❕Changeset is `enabled` and does **NOT** match with the pattern `${fileContent}`.") + } + } else { + markdownReason(project: args.project, reason: '* ❕Changeset is `disabled`.') + } + return false +} + +private Boolean whenComments(Map args = [:]) { + if (args.content?.get('comments') && env.GITHUB_COMMENT?.trim()) { + def match = args.content.get('comments').find { env.GITHUB_COMMENT?.toLowerCase()?.contains(it?.toLowerCase()) } + if (match) { + markdownReason(project: args.project, reason: "* ✅ Comment is `enabled` and matches with the pattern `${match}`.") + return true + } + markdownReason(project: args.project, reason: "* ❕Comment is `enabled` and does **NOT** match with the pattern `${args.content.get('comments').toString()}`.") + } else { + markdownReason(project: args.project, reason: '* ❕Comment is `disabled`.') + } + return false +} + +private boolean whenEnabled(Map args = [:]) { + return !args.content?.get('disabled') +} + +private Boolean whenLabels(Map args = [:]) { + if (args.content?.get('labels')) { + def match = args.content.get('labels').find { matchesPrLabel(label: it) } + if (match) { + markdownReason(project: args.project, reason: "* ✅ Label is `enabled` and matches with the pattern `${match}`.") + return true + } + markdownReason(project: args.project, reason: "* ❕Label is `enabled` and does **NOT** match with the pattern `${args.content.get('labels').toString()}`.") + } else { + markdownReason(project: args.project, reason: '* ❕Label is `disabled`.') + } + return false +} + +private Boolean whenParameters(Map args = [:]) { + if (args.content?.get('parameters')) { + def match = args.content.get('parameters').find { params[it] } + if (match) { + markdownReason(project: args.project, reason: "* ✅ Parameter is `enabled` and matches with the pattern `${match}`.") + return true + } else { + markdownReason(project: args.project, reason: "* ❕Parameter is `enabled` and does **NOT** match with the pattern `${args.content.get('parameters').toString()}`.") + } + } else { + markdownReason(project: args.project, reason: '* ❕Parameter is `disabled`.') + } + return false +} + +private Boolean whenTags(Map args = [:]) { + if (env.TAG_NAME?.trim() && args.content?.get('tags')) { + markdownReason(project: args.project, reason: '* ✅ Tag is `enabled`.') + return true + } + markdownReason(project: args.project, reason: '* ❕Tag is `disabled`.') + return false +} + +private void markdownReason(Map args = [:]) { + dir('build-reasons') { + def fileName = 'build.md' + def data = '' + if(fileExists(fileName)) { + data = readFile(file: "${fileName}") + } + def content = "${data}\r\n${args.reason}" + writeFile(file: fileName, text: "${content}") + } +} diff --git a/vars/beatsWhen.txt b/vars/beatsWhen.txt new file mode 100644 index 000000000..781fc3e5a --- /dev/null +++ b/vars/beatsWhen.txt @@ -0,0 +1,24 @@ +

+ Given the YAML definition and the changeset global macros + then it verifies if the project or stage should be enabled. + + The list of step's params and the related default values are: +

+

+ +
+    whenTrue(beatsWhen(project: 'auditbeat', changesetFunction: this.&getProjectDependencies
+                       content: readYaml(file: 'auditbeat/Jenkinsfile.yml')))
+        ...
+    }
+
+    def getProjectDependencies(Map args = [:]) {
+        ...
+    }
+