diff --git a/README.adoc b/README.adoc index 07be5ca37..3f2787ec2 100644 --- a/README.adoc +++ b/README.adoc @@ -189,18 +189,45 @@ Supported parameters: `imageName`:: Name of the docker image to build -`configs`:: -(Optional) extra flags - -registry: override the smart default of jenkinsciinfra/ or jenkins4eval/ -dockerfile: override the default dockerfile of Dockerfile +`config`:: +(Optional) map of extra flags + +* agentLabels: String expression for the labels the agent must match +* automaticSemanticVersioning: Do not automagically increase semantic version by default +* includeImageNameInTag: Set to true for multiple semversioned images built in parallel, will include the image name in tag to avoid conflict +* dockerfile: override the default dockerfile of Dockerfile +* targetplatforms: defined the platforms to build as TARGET +* nextVersionCommand: Commmand line used to retrieve the next version (default 'jx-release-version') +* gitCredentials: override Credential ID for tagging and creating release +* imageDir: Relative path to the context directory for the Docker build +* registryNamespace: empty = autodiscover based on the current controller, but can override the smart default of jenkinsciinfra/ or jenkins4eval/ +* unstash: Allow to unstash files if not empty +* dockerBakeFile: Allow to build from a bake file instead ==== Example [source, groovy] ---- -buildDockerImage_k8s('plugins-site-api') +buildDockerAndPublishImage('plugins-site-api') +buildDockerAndPublishImage('inbound-agent-maven:jdk8-nanoserver', [ + dockerfile: 'maven/jdk8/Dockerfile.nanoserver', + agentLabels: 'docker-windows-2019 && amd64', + targetplatforms: 'windows/amd64', + imageDir: 'maven/jdk8', + ]) +---- + +is also called from `parallelDockerUpdatecli` with `config` within `buildDockerConfig` like this : +[source, groovy] +---- +parallelDockerUpdatecli([ + imageName: 'wiki', + rebuildImageOnPeriodicJob: false, + updatecliConfig: [containerMemory: '1G'], + buildDockerConfig : [targetplatforms: 'linux/amd64,linux/arm64,linux/s390x'] +]) ---- + == Contribute === Requirements @@ -213,4 +240,3 @@ buildDockerImage_k8s('plugins-site-api') By adding `@Library('pipeline-library@pull//head') _` at the top of a Jenkinsfile from a repository built on one of the *.ci.jenkins.io instances, you can test your pipeline library pull request on ci.jenkins.io. A repository is dedicated for these kind of tests: https://github.com/jenkinsci/jenkins-infra-test-plugin/ - diff --git a/resources/io/jenkins/infra/docker/Makefile b/resources/io/jenkins/infra/docker/Makefile index ff2007c29..d9bba666f 100644 --- a/resources/io/jenkins/infra/docker/Makefile +++ b/resources/io/jenkins/infra/docker/Makefile @@ -9,11 +9,12 @@ endif IMAGE_NAME ?= helloworld IMAGE_DEPLOY_NAME ?= "$(IMAGE_NAME)" -IMAGE_PLATFORM ?= linux/amd64 +BUILD_TARGETPLATFORM ?= linux/amd64 # Paths IMAGE_DOCKERFILE ?= "$(IMAGE_DIR)"/Dockerfile HADOLINT_REPORT ?= "$(IMAGE_DIR)"/hadolint.json TEST_HARNESS ?= "$(IMAGE_DIR)"/cst.yml +DOCKER_BAKE_FILE ?= "$(IMAGE_DIR)"/docker-bake.hcl ## Image metadatas GIT_COMMIT_REV ?= $(shell git log -n 1 --pretty=format:'%h') @@ -47,11 +48,16 @@ build: ## Build the Docker Image $(IMAGE_NAME) from $(IMAGE_DOCKERFILE) --label "org.label-schema.vcs-ref=$(GIT_COMMIT_REV)" \ --label "org.opencontainers.image.created=$(BUILD_DATE)" \ --label "org.label-schema.build-date=$(BUILD_DATE)" \ - --platform "$(IMAGE_PLATFORM)" \ + --platform "$(BUILD_TARGETPLATFORM)" \ --file "$(call FixPath,$(IMAGE_DOCKERFILE))" \ "$(IMAGE_DIR)" @echo "== Build succeeded" +bake-build: ## Build the Docker Image(s) with dockerbake file + @echo "== Building from DockerBake file" + @docker buildx bake -f "$(call FixPath,$(DOCKER_BAKE_FILE))" + + clean: ## Delete any file generated during the build steps @echo "== Cleaning working directory $(IMAGE_DIR) from generated artefacts:" rm -f "$(call FixPath,$(IMAGE_DIR)/*.tar)" "$(HADOLINT_REPORT)" @@ -62,6 +68,13 @@ test: ## Execute the test harness on the Docker Image container-structure-test test --driver=docker --image="$(IMAGE_NAME)" --config="$(call FixPath,$(TEST_HARNESS))" @echo "== Test succeeded" +bake-test: ## Execute the test harness on the Docker Image with load + @echo "== Load $(IMAGE_NAME) within docker engine from docker bake buildx engine" + @docker buildx bake -f "$(call FixPath,$(DOCKER_BAKE_FILE))" --set "*.platform=linux/$(shell dpkg --print-architecture)" --load + @echo "== Test $(IMAGE_NAME) with $(call FixPath,$(TEST_HARNESS)) from $(IMAGE_NAME) with container-structure-test:" + container-structure-test test --driver=docker --image="$(IMAGE_NAME)" --config="$(call FixPath,$(TEST_HARNESS))" + @echo "== Test succeeded" + ## This steps expects that you are logged to the Docker registry to push image into deploy: ## Tag and push the built image as specified by $(IMAGE_DEPLOY). @echo "== Deploying $(IMAGE_NAME) to $(IMAGE_DEPLOY_NAME) with docker:" @@ -69,4 +82,8 @@ deploy: ## Tag and push the built image as specified by $(IMAGE_DEPLOY). docker image push "$(IMAGE_DEPLOY_NAME)" @echo "== Deploy succeeded" +bake-deploy: ## Tag and push the built image as specified by docker bake file + @echo "== Deploying with docker bake file" + @docker buildx bake -f "$(call FixPath,$(DOCKER_BAKE_FILE))" --push + .PHONY: all clean lint build test deploy diff --git a/resources/io/jenkins/infra/docker/jenkinsinfrabakefile.hcl b/resources/io/jenkins/infra/docker/jenkinsinfrabakefile.hcl new file mode 100644 index 000000000..7ac763408 --- /dev/null +++ b/resources/io/jenkins/infra/docker/jenkinsinfrabakefile.hcl @@ -0,0 +1,52 @@ +variable "IMAGE_DEPLOY_NAME" {} + +variable "REGISTRY" { + default = "docker.io" +} + +variable "TAG_NAME" { + default = "" +} + +variable "BAKE_TARGETPLATFORMS" { + default = "linux/arm64" +} + +variable "IMAGE_DOCKERFILE" { + default = "Dockerfile" +} + +variable "IMAGE_DIR" { + default = "." +} + +# return the full image name +function "full_image_name" { + params = [tag] + result = notequal("", tag) ? "${REGISTRY}/${IMAGE_DEPLOY_NAME}:${tag}" : "${REGISTRY}/${IMAGE_DEPLOY_NAME}:latest" +} + +target "default" { + dockerfile = IMAGE_DOCKERFILE + context = IMAGE_DIR + tags = [ + full_image_name("latest"), + full_image_name(TAG_NAME) + ] + platforms = [BAKE_TARGETPLATFORMS] + args = { + GIT_COMMIT_REV="$(GIT_COMMIT_REV)", + GIT_SCM_URL="$(GIT_SCM_URL)", + BUILD_DATE="$(BUILD_DATE)", + } + labels = { + "org.opencontainers.image.source"="$(GIT_SCM_URL)", + "org.label-schema.vcs-url"="$(GIT_SCM_URL)", + "org.opencontainers.image.url"="$(SCM_URI)", + "org.label-schema.url"="$(SCM_URI)", + "org.opencontainers.image.revision"="$(GIT_COMMIT_REV)", + "org.label-schema.vcs-ref"="$(GIT_COMMIT_REV)", + "org.opencontainers.image.created"="$(BUILD_DATE)", + "org.label-schema.build-date"="$(BUILD_DATE)", + } +} diff --git a/test/groovy/BuildDockerAndPublishImageStepTests.groovy b/test/groovy/BuildDockerAndPublishImageStepTests.groovy index 0c8d8367f..5cb4ade8f 100644 --- a/test/groovy/BuildDockerAndPublishImageStepTests.groovy +++ b/test/groovy/BuildDockerAndPublishImageStepTests.groovy @@ -113,14 +113,6 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { addEnvVar('TAG_NAME', gitTag) } - // Return if the set of methods expected for ALL pipeline run have been detected in the callstack - Boolean assertBaseWorkflow() { - return assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile') \ - && (assertMethodCallContainsPattern('sh','make lint') || assertMethodCallContainsPattern('powershell','make lint')) \ - && (assertMethodCallContainsPattern('sh','make build') || assertMethodCallContainsPattern('powershell','make build')) \ - && assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}") - } - // Return if the usual static checks had been recorded with the usual pattern Boolean assertRecordIssues(String imageName = fullTestImageName) { final String reportId = "${imageName}-hadolint-${mockedTimestamp}".replaceAll('/','-').replaceAll(':', '-') @@ -130,12 +122,6 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { ) } - // return if the "make deploy" was detected with the provided argument as image name - Boolean assertMakeDeploy(String expectedImageName = fullTestImageName) { - return (assertMethodCallContainsPattern('sh','make deploy') || assertMethodCallContainsPattern('powershell','make deploy')) \ - && assertMethodCallContainsPattern('withEnv', "IMAGE_DEPLOY_NAME=${expectedImageName}") - } - Boolean assertTagPushed(String newVersion) { return assertMethodCallContainsPattern('echo','Configuring credential.helper') \ && assertMethodCallContainsPattern('echo',"Tagging and pushing the new version: ${newVersion}") \ @@ -167,19 +153,24 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + assertTrue(assertMethodCallContainsPattern('node', 'docker')) // And the expected environment variable defined to their defaults assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DIR=.')) assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DOCKERFILE=Dockerfile')) - assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_PLATFORM=linux/amd64')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'BAKE_TARGETPLATFORMS=linux/amd64')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DEPLOY_NAME=' + fullTestImageName)) // And generated reports are recorded assertTrue(assertRecordIssues()) // And the deploy step called - assertTrue(assertMakeDeploy()) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) // And `unstash` isn't called assertFalse(assertMethodCall('unstash')) @@ -204,12 +195,18 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build with the code cloned assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + assertTrue(assertMethodCallContainsPattern('node', 'docker')) // And generated reports are recorded with named without ':' but '-' instead assertTrue(assertRecordIssues(fullCustomImageName.replaceAll(':','-'))) // With the deploy step called with the correct image name - assertTrue(assertMakeDeploy(fullCustomImageName)) + assertMethodCallContainsPattern('sh','make bake-deploy') + assertMethodCallContainsPattern('withEnv', "IMAGE_DEPLOY_NAME=${fullCustomImageName}") + // But no tag pushed assertFalse(assertTagPushed(defaultGitTag)) // And all mocked/stubbed methods have to be called @@ -230,12 +227,17 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build with the code cloned assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) assertTrue(assertMethodCallContainsPattern('node', 'docker')) // And generated reports are recorded assertTrue(assertRecordIssues()) // And the deploy step called - assertTrue(assertMakeDeploy()) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DEPLOY_NAME=' +fullTestImageName)) + // And the tag pushed assertTrue(assertTagPushed(defaultGitTag)) // But no release created (no tag triggering the build) @@ -259,12 +261,18 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build with the code cloned assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + assertTrue(assertMethodCallContainsPattern('node', 'docker')) // And generated reports are recorded assertTrue(assertRecordIssues()) // And the deploy step called - assertTrue(assertMakeDeploy()) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DEPLOY_NAME=' + fullTestImageName)) + // And the tag pushed assertTrue(assertTagPushed(defaultGitTagIncludingImageName)) // But no release created (no tag triggering the build) @@ -292,15 +300,21 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build with the code cloned assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + assertTrue(assertMethodCallContainsPattern('node', 'docker')) // And the environement variables set with the custom configuration values assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DIR=docker/')) assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DOCKERFILE=build.Dockerfile')) - assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_PLATFORM=linux/s390x')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'BAKE_TARGETPLATFORMS=linux/s390x')) assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_NAME=' + expectedImageName)) // But no tag and no deploy called (branch or PR) - assertTrue(assertMakeDeploy(expectedImageName)) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DEPLOY_NAME=' + expectedImageName)) + assertTrue(assertTagPushed(defaultGitTag)) // And all mocked/stubbed methods have to be called verifyMocks() @@ -319,10 +333,15 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + assertTrue(assertMethodCallContainsPattern('node', 'docker')) // But no deploy step called for latest - assertFalse(assertMakeDeploy()) + assertFalse(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'TAG_NAME=null')) // And no release (no tag) assertFalse(assertTagPushed(defaultGitTag)) // And all mocked/stubbed methods have to be called @@ -343,10 +362,17 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + assertTrue(assertMethodCallContainsPattern('node', 'docker')) // And the deploy step called for latest - assertTrue(assertMakeDeploy("${fullTestImageName}:${defaultGitTag}")) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DEPLOY_NAME=' + fullTestImageName)) + assertTrue(assertMethodCallContainsPattern('withEnv', 'TAG_NAME=' + defaultGitTag)) + // And the release is created (tag triggering the build) assertTrue(assertReleaseCreated()) // But no tag pushed @@ -376,10 +402,17 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + assertTrue(assertMethodCallContainsPattern('node', 'docker')) // And the deploy step called for latest - assertTrue(assertMakeDeploy("${fullTestImageName}:${defaultGitTag}")) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DEPLOY_NAME=' + fullTestImageName)) + assertTrue(assertMethodCallContainsPattern('withEnv', 'TAG_NAME=' + defaultGitTag)) + // And the release is not created as no next release draft exists assertFalse(assertReleaseCreated()) // But no tag pushed @@ -402,7 +435,10 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build with the code cloned assertJobStatusSuccess() // With the deploy step called with the correct image name - assertTrue(assertMakeDeploy("${fullCustomImageName}-${customGitTag}")) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', "IMAGE_DEPLOY_NAME=${fullCustomImageName}")) + assertTrue(assertMethodCallContainsPattern('withEnv', "TAG_NAME=${customGitTag}")) + // And all mocked/stubbed methods have to be called verifyMocks() } @@ -465,33 +501,6 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { verifyMocks() } - @Test - void itBuildsAndDeploysWithDockerEngineOnPrincipalBranch() throws Exception { - def script = loadScript(scriptName) - mockPrincipalBranch() - withMocks { - script.call(testImageName) - } - printCallStack() - // Then we expect a successful build with the code cloned - assertJobStatusSuccess() - // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) - assertTrue(assertMethodCallContainsPattern('node', 'docker')) - // And the expected environment variables set to their default values - assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DIR=.')) - assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DOCKERFILE=Dockerfile')) - assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_PLATFORM=linux/amd64')) - // And generated reports recorded - assertTrue(assertRecordIssues()) - // And the deploy step called - assertTrue(assertMakeDeploy()) - // But no release created automatically - assertFalse(assertTagPushed(defaultGitTag)) - // And all mocked/stubbed methods been called - verifyMocks() - } - @Test void itBuildsOnlyOnChangeRequestWithWindowsContainers() throws Exception { helper.registerAllowedMethod('isUnix', [], { false }) @@ -505,16 +514,24 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // Then we expect a successful build with the code cloned assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('powershell','make lint')) + assertTrue(assertMethodCallContainsPattern('powershell','make build')) + + assertTrue(assertMethodCallContainsPattern('node', 'docker-windows')) // And the expected environment variables set to their default values assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DIR=.')) assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DOCKERFILE=Dockerfile')) - assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_PLATFORM=linux/amd64')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'BAKE_TARGETPLATFORMS=linux/amd64')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_NAME=' + fullTestImageName)) // And generated reports recorded assertTrue(assertRecordIssues()) // But no deploy step called (not on principal branch) - assertFalse(assertMakeDeploy()) + assertFalse(assertMethodCallContainsPattern('sh','make deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'TAG_NAME=null')) + // But no release created automatically assertFalse(assertTagPushed(defaultGitTag)) // And all mocked/stubbed methods been called @@ -539,19 +556,24 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { assertJobStatusSuccess() // With the common workflow run as expected - assertTrue(assertBaseWorkflow()) + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + assertTrue(assertMethodCallContainsPattern('node', 'docker')) // And the expected environment variable defined to their defaults assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DIR=.')) assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DOCKERFILE=Dockerfile')) - assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_PLATFORM=linux/amd64')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'BAKE_TARGETPLATFORMS=linux/amd64')) // And generated reports are recorded assertTrue(assertRecordIssues()) // And the deploy step called - assertTrue(assertMakeDeploy()) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DEPLOY_NAME=' + fullTestImageName)) // And `unstash` is called assertTrue(assertMethodCallContainsPattern('unstash', 'stashName')) @@ -562,4 +584,189 @@ class BuildDockerAndPublishImageStepTests extends BaseTest { // And all mocked/stubbed methods have to be called verifyMocks() } + + @Test + void itBuildsAndDeploysImageWithCustomPlatformOnPrincipalBranch() throws Exception { + def script = loadScript(scriptName) + mockPrincipalBranch() + withMocks{ + script.call(testImageName, [ + platform: 'linux/amd64,linux/arm64,linux/s390x', + automaticSemanticVersioning: true, + ]) + } + printCallStack() + // Then we expect a successful build with the code cloned + assertJobStatusSuccess() + // With the common workflow run as expected + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + + assertTrue(assertMethodCallContainsPattern('node', 'docker')) + // And the environement variables set with the custom configuration values + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DIR=.')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DOCKERFILE=Dockerfile')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'PLATFORMS=linux/amd64,linux/arm64,linux/s390x')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_NAME=' + fullTestImageName)) + // But no tag and no deploy called (branch or PR) + assertTrue(assertMethodCallContainsPattern('sh','make bake-deploy')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DEPLOY_NAME=' + fullTestImageName)) + + assertTrue(assertTagPushed(defaultGitTag)) + // And all mocked/stubbed methods have to be called + verifyMocks() + } + + @Test + void itBuildsAndDeploysImageWithSpecifiedBakeFileOnPrincipalBranch() throws Exception { + def script = loadScript(scriptName) + mockPrincipalBranch() + withMocks{ + script.call(testImageName, [ + dockerBakeFile: 'bake.yml', + ]) + } + + //final String expectedImageName = 'jenkins/' + testImageName + printCallStack() + + // Then we expect a successful build with the code cloned + assertJobStatusSuccess() + + // // With the common workflow run as expected + assertTrue(assertMethodCallContainsPattern('libraryResource','io/jenkins/infra/docker/Makefile')) + assertTrue(assertMethodCallContainsPattern('withEnv', "BUILD_DATE=${mockedSimpleDate}")) + assertTrue(assertMethodCallContainsPattern('sh','make lint')) + assertTrue(assertMethodCallContainsPattern('sh','make bake-build')) + + assertTrue(assertMethodCallContainsPattern('sh', 'make bake-build')) + assertFalse(assertMethodCallContainsPattern('sh', 'make build')) + assertTrue(assertMethodCallContainsPattern('sh', 'make bake-deploy')) + assertFalse(assertMethodCallContainsPattern('sh', 'make deploy')) + // // And the environement variables set with the custom configuration values + assertTrue(assertMethodCallContainsPattern('withEnv', 'BAKE_TARGETPLATFORMS=linux/amd64')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'IMAGE_DOCKERFILE=Dockerfile')) + assertTrue(assertMethodCallContainsPattern('withEnv', 'DOCKER_BAKE_FILE=bake.yml')) + // // And all mocked/stubbed methods have to be called + verifyMocks() + } + + @Test + void itBuildWithWarningWithPlatform() throws Exception { + def script = loadScript(scriptName) + mockPrincipalBranch() + withMocks{ + script.call(testImageName, [ + platform: 'linux/amd64', + ]) + } + + printCallStack() + + // Then we expect a failing build + assertJobStatusSuccess() + + // And the error message is shown + assertTrue(assertMethodCallContainsPattern('echo', 'WARNING: `platform` is deprecated, use `targetplatforms` instead.')) + } + + @Test + void itFailWithBothPlatformAndTargetplatforms() throws Exception { + def script = loadScript(scriptName) + mockPrincipalBranch() + withMocks{ + script.call(testImageName, [ + platform: 'linux/amd64', + targetplatforms: 'linux/arm64', + ]) + } + + printCallStack() + + // Then we expect a failing build + assertJobStatusFailure() + + // And the error message is shown + assertTrue(assertMethodCallContainsPattern('echo', 'ERROR: Only one platform parameter is supported for now either platform or targetplatforms, prefer `targetplatforms`.')) + } + + @Test + void itFailWithWindowsAndMoreThanOnePlatform() throws Exception { + def script = loadScript(scriptName) + mockPrincipalBranch() + withMocks{ + script.call(testImageName, [ + agentLabels: 'docker-windows', + targetplatforms: 'linux/arm64,linux/amd64', + ]) + } + + printCallStack() + + // Then we expect a failing build + assertJobStatusFailure() + + // And the error message is shown + assertTrue(assertMethodCallContainsPattern('echo', 'ERROR: with windows, only one platform can be specified within targetplatforms.')) + } + + @Test + void itDontBuildsAndDeploysImageWithWindowsAndBakeOnPrincipalBranch() throws Exception { + def script = loadScript(scriptName) + mockPrincipalBranch() + withMocks{ + script.call(testImageName, [ + dockerBakeFile: 'bake.yml', + targetplatforms: 'windows/1804', + agentLabels: 'docker-windows', + ]) + } + + printCallStack() + + // Then we expect a failing build + assertJobStatusFailure() + + // And the error message is shown + assertTrue(assertMethodCallContainsPattern('echo', 'ERROR: dockerBakeFile is not supported on windows.')) + } + + @Test + void itWarnIfWindowsAgentAndNotWindowsTarget() throws Exception { + def script = loadScript(scriptName) + mockPrincipalBranch() + withMocks{ + script.call(testImageName, [ + targetplatforms: 'linux/amd64', + agentLabels: 'docker-windows', + ]) + } + printCallStack() + + // Then we expect a failing build + assertJobStatusSuccess() + + // And the error message is shown + assertTrue(assertMethodCallContainsPattern('echo', 'WARNING: A \'windows\' agent is requested, but the \'platform(s)\' is set to')) + } + + @Test + void itWarnIfNotWindowsAgentButWindowsTarget() throws Exception { + def script = loadScript(scriptName) + mockPrincipalBranch() + withMocks{ + script.call(testImageName, [ + targetplatforms: 'windows/1804', + ]) + } + printCallStack() + + // Then we expect a failing build + assertJobStatusSuccess() + + // And the error message is shown + assertTrue(assertMethodCallContainsPattern('echo', 'WARNING: The \'targetplatforms\' is set to \'windows/1804\', but there isn\'t any \'windows\' agent requested.')) + } } diff --git a/vars/buildDockerAndPublishImage.groovy b/vars/buildDockerAndPublishImage.groovy index 26f6012d3..15a541689 100644 --- a/vars/buildDockerAndPublishImage.groovy +++ b/vars/buildDockerAndPublishImage.groovy @@ -3,25 +3,57 @@ import java.text.SimpleDateFormat import java.util.Date import java.text.DateFormat +// makecall is a function to concentrate all the call to 'make' +def makecall(String action, String imageDeployName, String targetOperationSystem, String specificDockerBakeFile) { + final String bakefileContent = libraryResource 'io/jenkins/infra/docker/jenkinsinfrabakefile.hcl' + + // Please note that "make deploy" and the generated bake deploy file uses the environment variable "IMAGE_DEPLOY_NAME" + withEnv(["IMAGE_DEPLOY_NAME=${imageDeployName}", "TAG_NAME=${env.TAG_NAME}"]) { + if (isUnix()) { + if (! specificDockerBakeFile) { + specificDockerBakeFile = 'jenkinsinfrabakefile.hcl' + writeFile file: specificDockerBakeFile, text: bakefileContent + } + withEnv(["DOCKER_BAKE_FILE=${specificDockerBakeFile}"]) { + sh 'export BUILDX_BUILDER_NAME=buildx-builder; docker buildx use "${BUILDX_BUILDER_NAME}" 2>/dev/null || docker buildx create --use --name="${BUILDX_BUILDER_NAME}"' + sh "make bake-$action" + } + } else { + if (action == 'deploy') { + if (env.TAG_NAME) { + // User could specify a tag in the image name. In that case the git tag is appended. Otherwise the docker tag is set to the git tag. + if (imageDeployName.contains(':')) { + imageDeployName += "-${env.TAG_NAME}" + } else { + imageDeployName += ":${env.TAG_NAME}" + } + } + } + powershell "make $action" + } // unix agent + } // withEnv +} + def call(String imageShortName, Map userConfig=[:]) { def defaultConfig = [ agentLabels: 'docker || linux-amd64-docker', // String expression for the labels the agent must match automaticSemanticVersioning: false, // Do not automagically increase semantic version by default includeImageNameInTag: false, // Set to true for multiple semversioned images built in parallel, will include the image name in tag to avoid conflict dockerfile: 'Dockerfile', // Obvious default - platform: 'linux/amd64', // Intel/AMD 64 Bits, following Docker platform identifiers + targetplatforms: '', // // Define the (comma separated) list of Docker supported platforms to build the image for. Defaults to `linux/amd64` when unspecified. Incompatible with the legacy `platform` attribute. nextVersionCommand: 'jx-release-version', // Commmand line used to retrieve the next version gitCredentials: 'github-app-infra', // Credential ID for tagging and creating release imageDir: '.', // Relative path to the context directory for the Docker build registryNamespace: '', // Empty by default (means "autodiscover based on the current controller") unstash: '', // Allow to unstash files if not empty + dockerBakeFile: '', // Specify the path to a custom Docker Bake file to use instead of the default one ] - // Merging the 2 maps - https://blog.mrhaki.com/2010/04/groovy-goodness-adding-maps-to-map_21.html final Map finalConfig = defaultConfig << userConfig // Retrieve Library's Static File Resources final String makefileContent = libraryResource 'io/jenkins/infra/docker/Makefile' + final boolean semVerEnabledOnPrimaryBranch = finalConfig.automaticSemanticVersioning && env.BRANCH_IS_PRIMARY // Only run 1 build at a time on primary branch to ensure builds won't use the same tag when semantic versionning is activated @@ -33,23 +65,53 @@ def call(String imageShortName, Map userConfig=[:]) { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX") final String buildDate = dateFormat.format(now) + if (finalConfig.platform) { + if (finalConfig.targetplatforms) { + // only one platform parameter is supported both platform and platforms cannot be set at the same time + echo 'ERROR: Only one platform parameter is supported for now either platform or targetplatforms, prefer `targetplatforms`.' + currentBuild.result = 'FAILURE' + return + } + // if platform is set, I override platforms with it + finalConfig.targetplatforms = finalConfig.platform + echo "WARNING: `platform` is deprecated, use `targetplatforms` instead." + } + + // Default Value if targetplatforms is not set, I set it to linux/amd64 by default + if (finalConfig.targetplatforms == '') { + finalConfig.targetplatforms = 'linux/amd64' + } + // Warn about potential Linux/Windows contradictions between platform & agentLabels, and set the Windows config suffix for CST files + // for now only one platform possible per windows build ! String cstConfigSuffix = '' - if (finalConfig.agentLabels.contains('windows') || finalConfig.platform.contains('windows')) { - if (finalConfig.agentLabels.contains('windows') && !finalConfig.platform.contains('windows')) { - echo "WARNING: A 'windows' agent is requested, but the 'platform' is set to '${finalConfig.platform}'." + if (finalConfig.agentLabels.contains('windows') || finalConfig.targetplatforms.contains('windows')) { + if (finalConfig.targetplatforms.split(',').length > 1) { + echo 'ERROR: with windows, only one platform can be specified within targetplatforms.' + currentBuild.result = 'FAILURE' + return } - if (!finalConfig.agentLabels.contains('windows') && finalConfig.platform.contains('windows')) { - echo "WARNING: The 'platform' is set to '${finalConfig.platform}', but there isn't any 'windows' agent requested." + if (finalConfig.agentLabels.contains('windows') && !finalConfig.targetplatforms.contains('windows')) { + echo "WARNING: A 'windows' agent is requested, but the 'platform(s)' is set to '${finalConfig.targetplatforms}'." + } + if (!finalConfig.agentLabels.contains('windows') && finalConfig.targetplatforms.contains('windows')) { + echo "WARNING: The 'targetplatforms' is set to '${finalConfig.targetplatforms}', but there isn't any 'windows' agent requested." } cstConfigSuffix = '-windows' } - String operatingSystem = finalConfig.platform.split('/')[0] + String operatingSystem = finalConfig.targetplatforms.split('/')[0] + + if (operatingSystem == 'windows' && finalConfig.dockerBakeFile != '') { + echo 'ERROR: dockerBakeFile is not supported on windows.' + currentBuild.result = 'FAILURE' + return + } final InfraConfig infraConfig = new InfraConfig(env) final String defaultRegistryNamespace = infraConfig.getDockerRegistryNamespace() final String registryNamespace = finalConfig.registryNamespace ?: defaultRegistryNamespace final String imageName = registryNamespace + '/' + imageShortName + echo "INFO: Resolved Container Image Name: ${imageName}" node(finalConfig.agentLabels) { @@ -58,7 +120,8 @@ def call(String imageShortName, Map userConfig=[:]) { "IMAGE_NAME=${imageName}", "IMAGE_DIR=${finalConfig.imageDir}", "IMAGE_DOCKERFILE=${finalConfig.dockerfile}", - "IMAGE_PLATFORM=${finalConfig.platform}", + "BUILD_TARGETPLATFORM=${finalConfig.targetplatforms}", + "BAKE_TARGETPLATFORMS=${finalConfig.targetplatforms}", ]) { infra.withDockerPullCredentials{ String nextVersion = '' @@ -138,11 +201,7 @@ def call(String imageShortName, Map userConfig=[:]) { } // stage stage("Build ${imageName}") { - if (isUnix()) { - sh 'make build' - } else { - powershell 'make build' - } + makecall('build', imageName, operatingSystem, finalConfig.dockerBakeFile) } //stage // There can be 2 kind of tests: per image and per repository @@ -154,11 +213,7 @@ def call(String imageShortName, Map userConfig=[:]) { if (fileExists(testHarness)) { stage("Test ${testName} for ${imageName}") { withEnv(["TEST_HARNESS=${testHarness}"]) { - if (isUnix()) { - sh 'make test' - } else { - powershell 'make test' - } + makecall('test', imageName, operatingSystem, finalConfig.dockerBakeFile) } // withEnv } //stage } else { @@ -208,25 +263,8 @@ def call(String imageShortName, Map userConfig=[:]) { infra.withDockerPushCredentials{ if (env.TAG_NAME || env.BRANCH_IS_PRIMARY) { stage("Deploy ${imageName}") { - String imageDeployName = imageName - if (env.TAG_NAME) { - // User could specify a tag in the image name. In that case the git tag is appended. Otherwise the docker tag is set to the git tag. - if (imageDeployName.contains(':')) { - imageDeployName += "-${env.TAG_NAME}" - } else { - imageDeployName += ":${env.TAG_NAME}" - } - } - - withEnv(["IMAGE_DEPLOY_NAME=${imageDeployName}"]) { - // Please note that "make deploy" uses the environment variable "IMAGE_DEPLOY_NAME" - if (isUnix()) { - sh 'make deploy' - } else { - powershell 'make deploy' - } - } // withEnv - } //stage + makecall('deploy', imageName, operatingSystem, finalConfig.dockerBakeFile) + } } // if } // withDockerPushCredentials diff --git a/vars/buildDockerAndPublishImage.txt b/vars/buildDockerAndPublishImage.txt index 0543b0bfe..b337120d3 100644 --- a/vars/buildDockerAndPublishImage.txt +++ b/vars/buildDockerAndPublishImage.txt @@ -10,12 +10,14 @@ The following arguments are available for this function: * Boolean **automaticSemanticVersioning**: (Optional, defaults to "false") Should a release be created for every merge to the mainBranch. This uses "jx-release-version" to determine the version number based on the commit history. * Boolean **includeImageNameInTag**: (Optional, defaults to "false") Set to true for multiple semversioned images built in parallel, will include the image name in tag to avoid conflict * String **dockerfile**: (Optional, defaults to "Dockerfile") Relative path to the Dockerfile to use within the repository (Example: "build.dockerfile", "docker/Dockerfile"). - * String **platform**: (Optional, defaults to "linux/amd64") Name of the docker platform to use when building this image. For multiple platforms use a comma separated list (Example: "linux/amd64,linux/arm64"). + * String ~~**platform**~~: DEPRECATED (Optional, defaults to "linux/amd64") Name of the docker platform to use when building this image.. + * String **targetplatforms**: (Optional, defaults to "linux/amd64") Define the (comma separated) list of Docker supported platforms to build the image for. Defaults to `linux/amd64` when unspecified. Incompatible with the legacy `platform` attribute. * String **nextVersionCommand** (Optional, defaults to "jx-release-version") If "automaticSemanticVersioning" is set, this is the command to retrieve the next version (to use for tagging and releasing) * String **gitCredentials**: (Optional, defaults to "") If "automaticSemanticVersioning" is set, name of the credential to use when tagging the git repository. Support user/password or GitHub App credential types. * String **imageDir**: (Optional, defaults to ".", the parent directory of the Dockerfile) Path to a directory to use as build context (Example: "docker/", "python/2.7") ref: https://docs.docker.com/engine/reference/commandline/build/#description. * String **registry**: (Optional, defaults to "" which defines the registry based on the current controller setup) Container registry to deploy this image to (Example: "ghcr.io", "python/2.7"). * String **unstash**: (Optional, default to "") Allow to restore files from a previously saved stash if not empty, should contain the name of the last stashed as per https://www.jenkins.io/doc/pipeline/steps/workflow-basic-steps/#unstash-restore-files-previously-stashed. + * String **dockerBakeFile**: (Optionan, default to "") Specify the path to a custom Docker Bake file to use instead of the default one The lint phase generates a report when it fails, recorded by the hadolint tool in your Jenkins instance. @@ -56,7 +58,7 @@ parallel( buildDockerAndPublishImage('maven-jdk8-nanoserver', [ imageDir: 'maven/jdk8', dockerfile: 'Dockerfile.nanoserver', - platform: 'windows/amd64', + targetplatforms: 'windows/amd64', agentLabels: 'windows', automaticSemanticVersioning: true, includeImageNameInTag: true, @@ -64,3 +66,10 @@ parallel( }, ) + +Build of three linux images in parallel (imply docker bake inside) +

+buildDockerAndPublishImage('wiki',[
+  targetplatforms: 'linux/amd64,linux/arm64,linux/s390x'
+]),
+