package com.freeleaps.devops import com.freeleaps.devops.enums.ImageBuilderTypes class ImageBuilder { def steps def workspace def contextRoot def dockerfile def builderType // customized parameters def name def registry def repository def architectures def version def registryCredentialsId def buildxBuilderName ImageBuilder(steps, workspace, contextRoot, dockerfile, builderType) { this.steps = steps this.workspace = workspace this.contextRoot = contextRoot this.dockerfile = dockerfile this.builderType = builderType } def setManifestsOfImage(registry, repository, name, version) { if (registry == null || registry.isEmpty()) { steps.error("registry is empty") } this.registry = registry if (repository == null || repository.isEmpty()) { steps.error("repository is empty") } this.repository = repository if (name == null || name.isEmpty()) { steps.error("name is empty") } this.name = name if (version == null || version.isEmpty()) { steps.error("version is empty") } this.version = version } def setArchitectures(architectures) { if (architectures == null || architectures.isEmpty()) { steps.error("architectures is empty") } this.architectures = architectures if (builderType == ImageBuilderTypes.DOCKER_IN_DOCKER && architectures.size() > 1) { steps.log.warn("ImageBuilder", "If you want to build multi-arch images and using Docker in Docker (DIND) as builder, system will using buildx to replace build command.") steps.log.info("ImageBuilder", "Creating buildx builder with name: multiarch-builder-${name}") steps.sh "docker buildx create --use --name multiarch-builder-${name} --platform ${architectures.join(",")}" steps.log.info("ImageBuilder", "Inspecting buildx builder with name: multiarch-builder-${name}") steps.sh "docker buildx inspect --bootstrap" this.buildxBuilderName = "multiarch-builder-${name}" } } def useCredentials(registryCredentialsId) { if (registryCredentialsId == null || registryCredentialsId.isEmpty()) { steps.error("registryCredentialsId is empty") } this.registryCredentialsId = registryCredentialsId } def build() { try { steps.log.info("ImageBuilder", "Building image with ${builderType.builder}") steps.log.info("ImageBuilder", "Workspace sets to: ${workspace}") steps.log.info("ImageBuilder", "Using dockerfile at: ${dockerfile}, context root sets to: ${contextRoot}") if (architectures == null || architectures.isEmpty()) { steps.log.warn("ImageBuilder", "No architectures specified, using default amd64") architectures = ['linux/amd64'] } steps.withCredentials([steps.usernamePassword(credentialsId: registryCredentialsId, passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) { steps.log.info("ImageBuilder", "Authentication to ${registry}") switch(builderType) { case ImageBuilderTypes.DOCKER_IN_DOCKER: steps.sh "docker login -u ${steps.env.DOCKER_USERNAME} -p ${steps.env.DOCKER_PASSWORD} ${registry}" break case ImageBuilderTypes.KANIKO: def auth = "${steps.env.DOCKER_USERNAME}:${steps.env.DOCKER_PASSWORD}".bytes.encodeBase64().toString() steps.writeFile file: '/kaniko/.docker/config.json', text: """{ "auths": { "${registry}": { "auth": "${auth}" } } }""" break default: steps.error("Unsupported builder type: ${builderType.builder}") } } switch(builderType) { case ImageBuilderTypes.DOCKER_IN_DOCKER: steps.dir(workspace) { if (buildxBuilderName != null && !buildxBuilderName.isEmpty() && architectures.size() > 1) { steps.log.info("ImageBuilder", "Building image ${registry}/${repository}/${name} with architectures: ${architectures} using buildx builder: ${buildxBuilderName}, tag sets to ${version}") steps.log.info("ImageBuilder", "Set builder log level to plain...") steps.env.BUILDKIT_PROGRESS = "plain" steps.sh "docker buildx build --builder ${buildxBuilderName} --platform ${architectures.join(",")} -t ${registry}/${repository}/${name}:${version} -f ${dockerfile} --push ${contextRoot}" } else { architectures.each { architecture -> def archTag = architecture.split("/")[1] steps.log.info("ImageBuilder", "Building image ${registry}/${repository}/${name} with architectures: ${architectures}, tag sets to ${version}") steps.sh "docker build -t ${registry}/${repository}/${name}:${version}-${archTag} --platform ${architecture} -f ${dockerfile} ${contextRoot}" steps.sh "docker push ${registry}/${repository}/${name}:${version}-${archTag}" } } } break case ImageBuilderTypes.KANIKO: steps.dir(workspace) { architectures.each { architecture -> def archTag = architecture.split("/")[1] steps.log.info("ImageBuilder", "Building image ${registry}/${repository}/${name} with architectures: ${architectures}, tag sets to ${version}-${archTag}") steps.sh "/kaniko/executor --log-format text --context ${contextRoot} --dockerfile ${dockerfile} --destination ${registry}/${repository}/${name}:${version}-${archTag} --custom-platform ${architecture}" } } break default: steps.error("Unsupported builder type: ${builderType.builder}") } } catch (Exception e) { steps.log.error("ImageBuilder", "Failed to build image: ${e.message}") throw e } finally { if (buildxBuilderName != null && !buildxBuilderName.isEmpty() && architectures.size() > 1) { try { steps.log.info("ImageBuilder", "Cleaning up buildx builder: ${buildxBuilderName}") steps.sh "docker buildx rm ${buildxBuilderName} || true" } catch (Exception e) { steps.log.warn("ImageBuilder", "Failed to cleanup buildx builder: ${e.message}") } } } } }