From 1b34bd5df1b84a6b449e1022a33ddb62257dbf46 Mon Sep 17 00:00:00 2001 From: amorozov Date: Wed, 5 Mar 2025 01:36:24 +0300 Subject: [PATCH] feature: names refactoring, Jenkinsfile edit, example edit, bug fixes --- Example.Jenkinsfile | 60 +++++++++--------- ci.example.properties | 6 +- ...ocker_build.groovy => build_docker.groovy} | 2 +- ..._build_push_update_compose_via_ssh.groovy} | 63 ++++++++++++++----- ...roovy => release_docker_build_push.groovy} | 14 ++++- ...ase_gitea_artifacts_publish_docker.groovy} | 0 .../util/DockerBuildCommandFactory.groovy | 2 +- src/main/groovy/util/DockerImageNames.groovy | 5 -- src/main/groovy/util/DockerLogin.groovy | 17 ++++- src/main/groovy/util/GitTags.groovy | 11 +++- src/main/groovy/util/ScriptLog.groovy | 15 ++++- 11 files changed, 128 insertions(+), 67 deletions(-) rename src/main/groovy/script/{docker_build.groovy => build_docker.groovy} (93%) rename src/main/groovy/script/{deploy_compose_via_ssh.groovy => deploy_docker_build_push_update_compose_via_ssh.groovy} (56%) rename src/main/groovy/script/{docker_push.groovy => release_docker_build_push.groovy} (82%) rename src/main/groovy/script/{docker_gitea_release_publish.groovy => release_gitea_artifacts_publish_docker.groovy} (100%) diff --git a/Example.Jenkinsfile b/Example.Jenkinsfile index d709d16..4b0e2e5 100644 --- a/Example.Jenkinsfile +++ b/Example.Jenkinsfile @@ -16,6 +16,8 @@ pipeline { steps { script { env.CI_PROPERTIES_FILE_LOCATIONS = ".ci/ci.properties" + env.CI_DOCKER_REGISTRY_USERNAME = env.GITEA_USER + env.CI_DOCKER_REGISTRY_PASSWORD = env.GITEA_TOKEN } } } @@ -35,44 +37,46 @@ pipeline { stage('Build: Regular') { steps { // TODO: Костыль. Надо скрипты адаптировать под сборку "На каждый коммит" и "Для релизов" - runGroovy 'docker_build' + runGroovy 'build_docker' } } // Дополнительная сборка для релизов stage('Build: Release Binaries And Deploy Image') { when { - tag "release-*" + anyOf { + tag "release-*" + tag "binaries-*" + } } steps { script { - // Пример смены набора докерфайлов, которые используются по-умолчанию env.CI_DOCKER_FILES_PRESET="release" } - runGroovy 'docker_build' + runGroovy 'release_docker_gitea_publish' } } // Пушим собранный образ в Docker Registry - stage('Publish: Publish a docker image') { + stage('Publish: Release docker image') { when { - tag "release-*" + anyOf { + tag "release-*" + tag "docker-*" + } } steps { - // Переопределяем параметры скрипта из переменных окружения сборщика - script { - env.CI_DOCKER_REGISTRY_USERNAME = env.GITEA_USER - env.CI_DOCKER_REGISTRY_PASSWORD = env.GITEA_TOKEN - } - - runGroovy 'docker_push' + runGroovy 'release_docker_build_push' } } // Создаем в Gitea релиз с бинарями stage('Publish: Create gitea release') { when { - tag "release-*" + anyOf { + tag "release-*" + tag "binaries-*" + } } steps { // Переопределяем параметры скрипта из переменных окружения сборщика @@ -83,32 +87,24 @@ pipeline { env.CI_GITEA_TOKEN = env.GITEA_TOKEN } - runGroovy 'docker_gitea_release_publish' - } - } - - // Добавляем SSH профиль для последующего деплоя и проверяем его - stage('Prepare: Configuring SSH profile') { - when { - tag "release-*" - } - steps { - // Переопределяем параметры скрипта из переменных окружения сборщика - script { - env.CI_DEPLOY_SSH_PROFILE_PRIVATE_KEY_BASE64 = env.SSH_KEY_BASE64 - } - - runGroovy 'deploy_ssh_profile_setup' + runGroovy 'release_gitea_artifacts_publish_docker' } } // Подключаемся к серверу по SSH, обновляем тэг образа и перезапускаем stage('Deploy: Update PROD docker-compose environment tag') { when { - tag "release-*" + anyOf { + tag "release-*" + tag "deploy-*" + } } steps { - runGroovy 'deploy_compose_via_ssh' + script { + env.CI_DEPLOY_SSH_PROFILE_PRIVATE_KEY_BASE64 = env.SSH_KEY_BASE64 + } + runGroovy 'deploy_ssh_profile_setup' + runGroovy 'deploy_docker_build_push_update_compose_via_ssh' } } } diff --git a/ci.example.properties b/ci.example.properties index 6c9baba..299acb9 100644 --- a/ci.example.properties +++ b/ci.example.properties @@ -1,6 +1,6 @@ # Git - ci.git.tag.deploy.prefixes=release-, deploy- - ci.git.tag.release.prefixes=release- + ci.git.tag.deploy.prefixes=deploy-, release- + ci.git.tag.release.prefixes=release-, binaries-, docker- # Docker ci.docker.files.default=Deploy.Dockerfile @@ -31,5 +31,5 @@ ## Releases ci.release.artifacts.dockerfiles=Binaries.Extra.Dockerfile ci.release.artifacts.is-required=true - ci.release.artifacts.binaries.libc.dockerfile.grab=/build/app.linux-386 + ci.release.artifacts.binaries.extra.dockerfile.grab=/build/app.linux-386 ci.release.artifact.app.linux-386.name=my-application-name.linux-386 diff --git a/src/main/groovy/script/docker_build.groovy b/src/main/groovy/script/build_docker.groovy similarity index 93% rename from src/main/groovy/script/docker_build.groovy rename to src/main/groovy/script/build_docker.groovy index bfe0385..55a28e3 100644 --- a/src/main/groovy/script/docker_build.groovy +++ b/src/main/groovy/script/build_docker.groovy @@ -32,7 +32,7 @@ ScriptLog.printf "Building docker image without tags..." for (def dockerfileName : Dockerfiles.getPresetDockerfiles()) { def dockerCommand = DockerBuildCommandFactory.getBuildCommand(dockerfileName) - println "Using dockerfile '${dockerfileName}', full command: '$dockerCommand'" + ScriptLog.printf "Building dockerfile '${dockerfileName}', full command: '$dockerCommand'" sh dockerCommand } diff --git a/src/main/groovy/script/deploy_compose_via_ssh.groovy b/src/main/groovy/script/deploy_docker_build_push_update_compose_via_ssh.groovy similarity index 56% rename from src/main/groovy/script/deploy_compose_via_ssh.groovy rename to src/main/groovy/script/deploy_docker_build_push_update_compose_via_ssh.groovy index 8b0696d..3901d76 100644 --- a/src/main/groovy/script/deploy_compose_via_ssh.groovy +++ b/src/main/groovy/script/deploy_docker_build_push_update_compose_via_ssh.groovy @@ -1,7 +1,9 @@ package script import util.CIProperties +import util.DockerBuildCommandFactory import util.DockerImageNames +import util.DockerLogin import util.DockerTags import util.Dockerfiles @@ -36,15 +38,15 @@ if (deployGitTags.isNullOrEmpty()) { def deployDockerfiles = Dockerfiles.getDeployDockerfiles() def dockerComposeBaseCommand = CIProperties.findProperty("deploy.docker-compose.base-command") - .orElse("docker compose") + .orElse("docker compose") def dockerComposeDirectory = CIProperties.getProperty("deploy.docker-compose.dir") def dockerComposeFileName = CIProperties.findProperty("deploy.docker-compose.filename") - .orElse("docker-compose.yml") + .orElse("docker-compose.yml") def sshDeployProfile = CIProperties.findProperty("deploy.ssh.profile") - .orElse("deploy") + .orElse("deploy") if (deployGitTags.size() > 1) { throw new IllegalStateException("Can not deploy more than one git tag via docker-compose. Found multiple deploy tags: ${deployGitTags}") @@ -54,6 +56,8 @@ def deployGitTag = deployGitTags.first() ScriptLog.printf "Starting, total ${deployDockerfiles.size()} dockerfiles marked to be deployed... (tag: ${deployGitTag}, dockerfiles: ${deployDockerfiles})" +DockerLogin.perform() + for (def dockerfileName : deployDockerfiles) { ScriptLog.printf "Deploying image for dockerfile named '${dockerfileName}' via ssh profile ${sshDeployProfile}" @@ -61,25 +65,52 @@ for (def dockerfileName : deployDockerfiles) { ScriptLog.printf "Docker image ${dockerImageToUpdateTagBaseName} base name will be used for dockerfile named '${dockerfileName}'" - def dockerReleaseTagToSetup = DockerTags.getDockerTagPostfixForDockerfile( - GitTags.sanitizeTagFromPrefixes(deployGitTag, GitTags.deployPrefixes) + def dockerReleaseTagToSetup = DockerTags.getDockerTagForDockerfile( + dockerfileName, + GitTags.sanitizeTagFromPrefixes(deployGitTag, GitTags.deployPrefixes) ) + def dockerReleaseImageToSetup = "${dockerImageToUpdateTagBaseName}:${dockerReleaseTagToSetup}" + + def dockerImageBuildCommand = DockerBuildCommandFactory.getBuildCommand(dockerfileName, "-t ${dockerReleaseImageToSetup}") + ScriptLog.printf "Rebuilding docker image '${dockerReleaseImageToSetup}' before deploy by command '${dockerImageBuildCommand}'..." + + sh dockerImageBuildCommand + + ScriptLog.printf "Pushing docker image '${dockerReleaseImageToSetup}' before deploy..." + sh "docker push ${dockerReleaseImageToSetup}" + ScriptLog.printf "Updating docker-compose image tag to '${dockerReleaseTagToSetup}' in service '${dockerImageToUpdateTagBaseName}' in file '${dockerComposeFileName}' located in directory '${dockerComposeDirectory}'..." - sh """ ssh -tt ${sshDeployProfile} "\\ - cd '${dockerComposeDirectory}' && \\ - sed -i 's|image: ${dockerImageToUpdateTagBaseName}:.*|image: ${dockerImageToUpdateTagBaseName}:${dockerReleaseTagToSetup}|g' '${dockerComposeFileName}'\\ - " - """ + if (dockerReleaseTagToSetup.isNullOrBlank()) { + throw new IllegalStateException("Build tag was not resolved (empty or null '${dockerReleaseTagToSetup}')") + } - ScriptLog.printf "Service tag updated, restarting compose..." + def dockerComposeRollbackFileName = "${dockerComposeFileName}.backup.${System.currentTimeMillis()}.yml" - sh """ ssh -tt ${sshDeployProfile} "\\ - cd '${dockerComposeDirectory}' && \\ - ${dockerComposeBaseCommand} -f '${dockerComposeFileName}' up -d \\ - " - """ + try { + sh """ ssh -tt ${sshDeployProfile} "\\ + cd '${dockerComposeDirectory}' && \\ + cp ${dockerComposeFileName} ${dockerComposeRollbackFileName} && \\ + sed -i 's|image: ${dockerImageToUpdateTagBaseName}:.*|image: ${dockerReleaseImageToSetup}|g' '${dockerComposeFileName}'\\ && + ${dockerComposeBaseCommand} -f '${dockerComposeFileName}' up -d \\ + " + """ + } catch (Throwable e) { + ScriptLog.printf("Exception occurred while updating docker-compose tag on deploy, rolling back") + e.printStackTrace() + + sh """ ssh -tt ${sshDeployProfile} "\\ + cd '${dockerComposeDirectory}' && \\ + rm ${dockerComposeFileName} && \\ + cp ${dockerComposeRollbackFileName} ${dockerComposeFileName} \\ + " + """ + + ScriptLog.printf("Application restored from backup") + + throw e + } ScriptLog.printf "Deploying of docker tag '${dockerReleaseTagToSetup}' successfully completed!" } \ No newline at end of file diff --git a/src/main/groovy/script/docker_push.groovy b/src/main/groovy/script/release_docker_build_push.groovy similarity index 82% rename from src/main/groovy/script/docker_push.groovy rename to src/main/groovy/script/release_docker_build_push.groovy index 9a9faf0..1974ae8 100644 --- a/src/main/groovy/script/docker_push.groovy +++ b/src/main/groovy/script/release_docker_build_push.groovy @@ -1,7 +1,9 @@ package script +import util.CIProperties import util.DockerImageNames + /** * Скрипт для публикации докер образа при сборке. */ @@ -11,8 +13,10 @@ import util.DockerImageNames @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-shell', version='2.0.2-SNAPSHOT', changing = true), @Grab(group='io.tswf.groovy.better-groovy', module='better-groovy-scripting-gitea', version='2.0.2-SNAPSHOT', changing = true) ]) +@groovy.transform.CompileStatic import util.DockerLogin import util.DockerBuildCommandFactory +import util.DockerTags import util.Dockerfiles import util.GitTags import util.GlobalProperties @@ -39,7 +43,7 @@ ScriptLog.printf "Starting docker publishing..." DockerLogin.perform() -def dockerfilesToPush = System.findGlobalPropertyList("ci.docker.image.push.files").orElse([]) +def dockerfilesToPush = CIProperties.findListProperty("docker.image.push.files").orElse([]) gitReleaseTags.each { gitTag -> Dockerfiles.getPresetDockerfiles().each { dockerfileName -> @@ -53,8 +57,12 @@ gitReleaseTags.each { gitTag -> def dockerImageBaseName = DockerImageNames.getImageName(dockerfileName) // Для докер-тегов образов используем гит-теги с отброшенным префиксом - def imageBaseTag = gitTag.replace(GitTags.getPrefix(), "") - def imageTag = DockerImageNames.getImageTag(imageBaseTag, dockerfileName) + def imageBaseTag = GitTags.sanitizeTagFromPrefixes(gitTag, GitTags.getReleasePrefixes()) + def imageTag = DockerTags.getDockerTagForDockerfile(imageBaseTag, dockerfileName) + + if (imageTag.isNullOrBlank()) { + throw new IllegalStateException("Build tag was not resolved (empty or null '${imageTag}')") + } def imageFullName = "${dockerImageBaseName}:${imageTag}" diff --git a/src/main/groovy/script/docker_gitea_release_publish.groovy b/src/main/groovy/script/release_gitea_artifacts_publish_docker.groovy similarity index 100% rename from src/main/groovy/script/docker_gitea_release_publish.groovy rename to src/main/groovy/script/release_gitea_artifacts_publish_docker.groovy diff --git a/src/main/groovy/util/DockerBuildCommandFactory.groovy b/src/main/groovy/util/DockerBuildCommandFactory.groovy index 3257029..b1fa140 100644 --- a/src/main/groovy/util/DockerBuildCommandFactory.groovy +++ b/src/main/groovy/util/DockerBuildCommandFactory.groovy @@ -12,7 +12,7 @@ class DockerBuildCommandFactory { ] return propertyNames.stream() - .map { CIProperties.findProperty("ci.docker.build.additional-args").orNull() } + .map { CIProperties.findProperty(it).orNull() } .filter { it != null } .findFirst() .orElse("") diff --git a/src/main/groovy/util/DockerImageNames.groovy b/src/main/groovy/util/DockerImageNames.groovy index e8a0b20..bb5d53c 100644 --- a/src/main/groovy/util/DockerImageNames.groovy +++ b/src/main/groovy/util/DockerImageNames.groovy @@ -5,11 +5,6 @@ import groovy.transform.CompileStatic @CompileStatic class DockerImageNames { - static String getImageTag(String baseName, String dockerfile) { - def imageTagPostfix = CIProperties.findProperty("docker.image.tag.${dockerfile.toLowerCase()}.postfix").orElse("") - return "${baseName}${imageTagPostfix}" - } - static String getImageName(String dockerfileName) { def propertyNames = [ "docker.image.${dockerfileName.toLowerCase()}.name", diff --git a/src/main/groovy/util/DockerLogin.groovy b/src/main/groovy/util/DockerLogin.groovy index 20b8e5a..d4eb696 100644 --- a/src/main/groovy/util/DockerLogin.groovy +++ b/src/main/groovy/util/DockerLogin.groovy @@ -18,9 +18,20 @@ class DockerLogin { // Логинимся в Registry ScriptLog.printf "Performing login to registry..." - def registryName = System.getGlobalProperty("ci.docker.registry") - def registryUser = System.getGlobalProperty("ci.docker.registry.username") - def registryPassword = System.getGlobalProperty("ci.docker.registry.password") + + def registryName = CIProperties.getProperty("docker.registry") + def registryUser = CIProperties.getProperty("docker.registry.username") + def registryPassword = CIProperties.getProperty("docker.registry.password") + + if (registryName.isNullOrBlank()) { + throw new IllegalStateException("Docker registry name not set") + } + if (registryUser.isNullOrBlank()) { + throw new IllegalStateException("Docker registry user not set") + } + if (registryPassword.isNullOrBlank()) { + throw new IllegalStateException("Docker registry password not set") + } sh "docker login $registryName -u $registryUser -p $registryPassword" diff --git a/src/main/groovy/util/GitTags.groovy b/src/main/groovy/util/GitTags.groovy index 8cca2ba..09ff59d 100644 --- a/src/main/groovy/util/GitTags.groovy +++ b/src/main/groovy/util/GitTags.groovy @@ -38,7 +38,16 @@ class GitTags { return getTagsWithPrefix(deployPrefixes) } - static String sanitizeTagFromPrefixes(String tag, Collection prefixes) { + static List getAllPrefixes() { + def result = new ArrayList() + + result.addAll(getReleasePrefixes()) + result.addAll(getDeployPrefixes()) + + return result + } + + static String sanitizeTagFromPrefixes(String tag, Collection prefixes = getAllPrefixes()) { return tag.removeAll(prefixes) } } diff --git a/src/main/groovy/util/ScriptLog.groovy b/src/main/groovy/util/ScriptLog.groovy index 3c0feff..c128970 100644 --- a/src/main/groovy/util/ScriptLog.groovy +++ b/src/main/groovy/util/ScriptLog.groovy @@ -1,8 +1,19 @@ package util +import groovy.transform.CompileStatic + +@CompileStatic class ScriptLog { static void printf(Object text, Object... args) { - def callerClassName = new Throwable().getStackTrace()[1].getClassName().split("\\.").last() - println "[${callerClassName}]: ${String.format(String.valueOf(text), args)}" + def scriptPrefix = new Throwable().getStackTrace() + .stream() + .map { it.className } + .filter { it != null } + .map { it.split("\\.").last() } + .filter { it.isNotNullOrBlank() && it.charAt(0).isLowerCase() } + .toList() + .lastOrNull() ?: "ci script" + + println "[${scriptPrefix}]: ${String.format(String.valueOf(text), args)}" } }