使用Jenkins更新Openshift上的应用
前言
本篇介绍了一种利用Jenkins Pipeline脚本更新Openshift容器云平台上已部署应用的方式,主要涉及如下知识:
- 如何利用Openshift在Jenkins的插件更新部署并获取状态
- 如何编写Jenkins Pipeline脚本
- 如何为Pipeline脚本建立动态参数、动态步骤
- 如何让Pipeline步骤并行执行
- 如何从容器镜像库获取镜像Tag列表
以下为运行效果示意,如感兴趣可继续阅读。
说明
本例中使用的Jenkins运行于Openshift平台上,可能是Openshift插件可以正常使用的条件。
本例中脚本对镜像Tag个格式有具体要求,详见下文。
如何更新
了解K8S和Openshift的同学应该知道,我们把StatefulSet、Deployment、DeploymentConfig里容器镜像替换掉,就会触发部署。
在以下Groovy方法中,调用了Openshift在Jenkins的插件的selector方法,找到部署对象,然后修改目标容器的镜像,从而触发更新。
def patchImage(appConfig) {
def p = openshift.selector("${appConfig.deploymentType}/${appConfig.deploymentName}").object()
def originalImage = p.spec.template.spec.containers[0].image
def targetImage = "image-registry.openshift-image-registry.svc:5000/${openshift.project()}/${appConfig.imageStreamName}:${appConfig.imageTag}"
p.spec.template.spec.containers[0].image = targetImage
openshift.apply(p)
if (targetImage != originalImage) {
echo "${appConfig.deploymentType}/${appConfig.deploymentName} 镜像已由 ${originalImage} 更新为: ${openshift.project()}/${appConfig.imageStreamName}:${appConfig.imageTag},将自动触发Rollout"
} else {
echo "${appConfig.deploymentType}/${appConfig.deploymentName} 镜像未改变: ${originalImage},将强制rollout。"
openshift.selector("${appConfig.deploymentType}/${appConfig.deploymentName}").rollout().latest()
}
}如何判断更新状态
Deployment在Rollout的过程中,我们可以通过openshift插件获取拿到最新的部署版本,并判断其是否更新完成。
由于DeploymentConfig(简写为dc)和StatefulSet(简写为sts)有些不同,所以,我们分开进行判断。
对于dc,主要通过比较desiredReplicas(期望副本数)和readyReplicas(就绪副本数)来判断是否完成了更新。
if (appConfig.deploymentType == "dc") {
def latestDeploymentVersion = openshift.selector(appConfig.deploymentType, appConfig.deploymentName).object().status.latestVersion
def rc = openshift.selector('rc', "${appConfig.deploymentName}-${latestDeploymentVersion}")
timeout (time: 30, unit: 'MINUTES') {
rc.untilEach(1){
def rcMap = it.object()
def desiredReplicas = rcMap.metadata.annotations['kubectl.kubernetes.io/desired-replicas']
if (desiredReplicas == null) {
desiredReplicas = rcMap.status.replicas
}
def readyReplicas = rcMap.status.readyReplicas == null ? 0 : rcMap.status.readyReplicas
echo "desired replicas: ${desiredReplicas}, readyReplicas: ${readyReplicas}"
if (waitEachCopy) {
return desiredReplicas != null && (desiredReplicas.equals(readyReplicas))
} else {
return desiredReplicas != null && readyReplicas >=1
}
}
}
}对于sts,方式是判断每个容器均Ready。
if (appConfig.deploymentType == "sts") {
def latestStsVersion = openshift.selector(appConfig.deploymentType, appConfig.deploymentName).object().status.updateRevision
def stsPods = openshift.selector('pods', [ 'controller-revision-hash': latestStsVersion ])
timeout (time: 30, unit: 'MINUTES') {
stsPods.untilEach(1){
if (waitEachCopy) {
return it.object().status.containerStatuses.every {
echo "pod container ready: ${it.ready}"
it.ready
}
} else {
return it.object().status.containerStatuses.any {
echo "pod container ready: ${it.ready}"
it.ready
}
}
}
}
}如何将镜像导入内部镜像库
在更新部署对象的镜像之前,我们可以先把容器镜像由外部镜像库导入到Openshift内部镜像库,可以多留存一份镜像,同时利于工作节点快速获取镜像(未验证)。
这里用到了openshit插件的raw方法,可以执行原生oc命令,所以导入镜像实际使用的命令为oc image mirror。不知什么原因,这里经常会报Unknown Blob错误,多试几次就好了。
在同步镜像之前,需要登录源和目标镜像仓库,使用的也是oc命令registry login。
Closure createImportImageStep(appConfig) {
return {
echo "当前集群:${openshift.cluster()} 当前项目: ${openshift.project()}"
echo "登录内部镜像库..."
openshift.raw("registry login --registry=${internalRegistryAddress} --insecure=true --skip-check=true")
echo "登录外部镜像库..."
openshift.raw("registry login --registry ${externalRegistryAddress} --auth-basic=YourUsername:YourPassword --insecure=true")
echo "导入镜像到内部镜像库..."
if (!appConfig.imageTag) {
echo "${appConfig.app} 因无目标镜像跳过镜像导入"
return
}
retry(5) {
importDockerImage(appConfig)
}
}
}
def importDockerImage(appConfig) {
echo "开始导入镜像:istag/${appConfig.imageStreamName}:${appConfig.imageTag}"
def p = openshift.selector("istag/${appConfig.imageStreamName}:${appConfig.imageTag}")
if (p.exists() && importImageForce) {
p.delete()
echo "imagestreamtag/${appConfig.imageStreamName}:${appConfig.imageTag} deleted."
}
if (!p.exists() || importImageForce) {
openshift.raw("image mirror ${externalRegistryAddress}/${appConfig.imagePath}/${appConfig.imageName}:${appConfig.imageTag} ${internalRegistryAddress}/${openshift.project()}/${appConfig.imageStreamName}:${appConfig.imageTag} --insecure=true")
}
echo "完成导入镜像:istag/${appConfig.imageStreamName}:${appConfig.imageTag}"
}如何列举镜像Tag
为了方便运维同事通过界面选择要更新的应用和Tag,需要Pipeline脚本从外部Docker镜像库读取各应用镜像的Tag。
这里脚本的核心点是调用了镜像库的HTTP API。
//访问镜像库api获取镜像Tag
def getDockerTags(appConfig) {
def url = "http://${externalRegistryAddress}/v2/${appConfig.imagePath}/${appConfig.imageName}/tags/list"
def list = getDockerImageTags(url)
list
}
// 分为3步,获得json文本,转换为json对象,从对象获得所有Tag
def getDockerImageTags(url) {
def myjson = getUrl(url)
def json = jsonParse(myjson);
def tags = json.tags
tags
}
//通过curl访问镜像库API,将结果保存为result.json文件,然后返回文件内容
def getUrl(url) {
sh(returnStdout: true, script: "curl -s --max-time 15 --retry 3 --retry-delay 5 ${url} 2>&1 | tee result.json")
def data = readFile('result.json').trim()
data
}
// 将镜像Tag倒序排列后,取前5个
def getLatestTag(appConfig) {
def tags = getDockerTags(appConfig)
if (tags == null || tags.size() == 0) {
return null
}
def sortedTags = sortReverse(tags)
def topTags = sortedTags.size() > 5 ? new ArrayList(sortedTags.subList(0,4)) : sortedTags
topTags
}
// 对镜像Tag按倒序排列,要求镜像Tag遵循一定格式,如2.1.0-20211105
@NonCPS
def sortReverse(list) {
list.sort{a,b ->
def ay = a.split("[.-]")
def by = b.split("[.-]")
for (int i = 0; i < ay.length; i++) {
def ai = ay[i].isInteger() ? ay[i].toInteger() : ay[i];
def bi = by[i].isInteger() ? by[i].toInteger() : by[i];
if (bi.compareTo(ai) == 0) {
continue
}
return bi.compareTo(ai)
}
}
}
// 将json文本转换为json对象
@NonCPS
def jsonParse(json) {
new groovy.json.JsonSlurperClassic().parseText(json)
}
如何生成选型参数
为Jenkins Pipeline创建选项参数,方法是创建一个ChoiceParameterDefinition的数组就可以了。
//生成镜像Tag选择器
def getTagChoiceParamter() {
def tags = []
tagSet.each { entry -> tags.add(entry.key) }
def sortedTags = sortReverse(tags)
def parameters = []
def para = new ChoiceParameterDefinition("latestTag", sortedTags, sortedTags[0], "最新版本")
parameters.add para
parameters
}
//这个是镜像选择的stage 片段
stage('选择版本') {
def tagSelectorInputTimeout = false
try {
timeout(time: 180, unit: 'SECONDS') {
tagSelectorInput = input(
id: 'tagInput', ok: '继续', message: '请选择要更新到的镜像Tag:', parameters: tagParameters
)
}
} catch(err) {
def user = err.getCauses()[0].getUser()
if('SYSTEM' == user.toString()) {
tagSelectorInputTimeout = true
} else {
tagSelectorInput = null
echo "镜像Tag选择被用户中止: [${user}]"
}
}
}验证身份
能够访问Pipeline,说明已经当前用户已经登录Jenkins了。为了多一道安全控制,需要用户在Openshift界面或通过命令oc whoami -t获取一个Token,在正式运行Pipeline部署步骤之前验证一下身份。
这里主要使用了Openshift插件的withCluster、withCredentials和raw三个方法,通过执行oc status -v命令验证其身份。
stage('登录Openshift') {
timeout(time: 180, unit: 'SECONDS') {
myToken = input(
id: 'myTokenInput', ok: '继续', message: '请输入访问Openshift的Token', parameters: [password(defaultValue: '', description: 'OCP访问令牌,可以在登录openshift后,通过执行 oc whoami -t 获得', name: 'Token for Openshift')]
)
}
openshift.withCluster() {
openshift.withCredentials(myToken) {
def status = openshift.raw('status', '-v')
echo "登录成功. 集群是:${openshift.cluster()} 默认项目: ${openshift.project()} 流水线版本:${pipelineVersion}"
}
}
}应用元数据
应用的列表和应用之间的依赖关系,以及各应用部署对象的类型和名称、镜像库路径等等,通过在脚本定义字段实现。
@Field def final myConfigJson = '''
[
{
"app": "activity", //应用
"imagePath": "xxx-group/release/yyy", //应用镜像在镜像库的路径
"imageName": "activity", //应用镜像的名称
"imageStreamName": "activity", //应用镜像在内部镜像库的名称
"deploymentType": "dc", //应用的部署类型,dc即DeploymentConfig
"deploymentName": "activity", //应用的Deployment或StatefulSet的名称
"selected": false, //是否默认选中,如果经常更新该应用,可默认选中
"dependencies": [
], //依赖的其它应用,可以控制更新顺序
"status": "new", //更新状态控制,这里都是new,其它由脚本控制
"imageTag": "", //目标镜像Tag,在运行中赋值
"group": ["积分活动"] //应用组,方便一次性选择多个应用,当前脚本以无此功能
},
{
"app": "point",
"imagePath": "xxx-group/release/yyy",
"imageName": "point",
"imageStreamName": "point",
"deploymentType": "dc",
"deploymentName": "point",
"selected": false,
"dependencies": [
],
"status": "new",
"imageTag": "",
"group": ["积分活动"]
}
]
'''完整脚本
完整的Pipeline脚本如下,为方便理解,加了一些注释。为方便选择应用,选择应用的方式几经修改,部分方法可能已经不再使用了。使用该脚本在Jenkins建立Pipeline类型的Job即可,不要选择“Use Groovy Sandbox”。可能因安全原因,脚本变更后,每次都需要Jenkins管理员授权。
import groovy.transform.Field
@Field def final pipelineVersion = "20210813" //此Pipeline的版本,用于发生问题时调查依据
@Field def final envType = "" // 环境类型 ".test" 为测试环境,""为生产环境,方便在测试环境和生产环境同时使用
@Field def final externalRegistryAddress = "docker-app.nexus${envType}.ccc" //外部镜像库域名
@Field def final internalRegistryAddress = "default-route-openshift-image-registry.apps.ocp${envType}.ccc" //内部镜像库域名
@Field def ocp_project = 'yyy-prod' //针对哪个Openshift Project
//应用元数据
@Field def final myConfigJson = '''
[
{
"app": "activity", //应用
"imagePath": "xxx-group/release/yyy", //应用镜像在镜像库的路径
"imageName": "activity", //应用镜像的名称
"imageStreamName": "activity", //应用镜像在内部镜像库的名称
"deploymentType": "dc", //应用的部署类型,dc即DeploymentConfig
"deploymentName": "activity", //应用的Deployment或StatefulSet的名称
"selected": false, //是否默认选中,如果经常更新该应用,可默认选中
"dependencies": [
], //依赖的其它应用,可以控制更新顺序
"status": "new", //更新状态控制,这里都是new,其它由脚本控制
"imageTag": "", //目标镜像Tag,在运行中赋值
"group": ["积分活动"] //应用组,方便一次性选择多个应用,当前脚本以无此功能
},
{
"app": "point",
"imagePath": "xxx-group/release/yyy",
"imageName": "point",
"imageStreamName": "point",
"deploymentType": "dc",
"deploymentName": "point",
"selected": false,
"dependencies": [
],
"status": "new",
"imageTag": "",
"group": ["积分活动"]
}
]
'''
@Field def myConfig;
//Openshift Token,二次验证身份用
@Field def myToken
@Field def tagSet = [:] //镜像Tag的Map,key为镜像Tag,value为应用列表
@Field def rolloutGroupSelector //应用组选择器,此版本已废弃
@Field def rolloutAppSelector //应用选择器
@Field def tagParameters //镜像Tag参数
//运行选项
@Field def waitEachCopy = true //等待每个副本更新完成
@Field def waitDepen = true //等待依赖应用更新完成
@Field def importImageForce = false //当镜像已存在于内部镜像库时,强制导入
@Field def stepsForImportImages = [:] //导入镜像的Pipeline步骤,用于并行执行
@Field def stepsForUpdateApp = [:] //更新应用的Pipeline步骤,用于并行执行
node {
stage('登录Openshift') {
timeout(time: 180, unit: 'SECONDS') {
myToken = input(
id: 'myTokenInput', ok: '继续', message: '请输入访问Openshift的Token', parameters: [password(defaultValue: '', description: 'OCP访问令牌,可以在登录openshift后,通过执行 oc whoami -t 获得', name: 'Token for Openshift')]
)
}
openshift.withCluster() {
openshift.withCredentials(myToken) {
def status = openshift.raw('status', '-v')
echo "登录成功. 集群是:${openshift.cluster()} 默认项目: ${openshift.project()} 流水线版本:${pipelineVersion}"
}
}
myConfig = jsonParse(myConfigJson)
getTagSet(myConfig)
tagParameters = getTagChoiceParamter()
}
stage('选择版本') {
def tagSelectorInputTimeout = false
try {
timeout(time: 180, unit: 'SECONDS') {
tagSelectorInput = input(
id: 'tagInput', ok: '继续', message: '请选择要更新到的镜像Tag:', parameters: tagParameters
)
}
} catch(err) {
def user = err.getCauses()[0].getUser()
if('SYSTEM' == user.toString()) {
tagSelectorInputTimeout = true
} else {
tagSelectorInput = null
echo "镜像Tag选择被用户中止: [${user}]"
}
}
if (tagSelectorInputTimeout) {
currentBuild.result = 'FAILURE'
error "选择镜像Tag超时"
} else if (tagSelectorInput == null) {
currentBuild.result = 'FAILURE'
error "选择镜像错误,可能被用户中止"
} else {
echo "成功选择镜像标签"
}
}
stage('选择应用') {
def selectedTag = tagSelectorInput
def rolloutAppSelectorTimeout = false
try {
def paras = []
def appSelectedFollowTag = tagSet.get(selectedTag)
for (i=0; i < appSelectedFollowTag.size(); i++) {
paras.add booleanParam(defaultValue: true, description: '', name: appSelectedFollowTag[i].app)
}
timeout(time: 180, unit: 'SECONDS') {
rolloutAppSelector = input(id: 'rolloutAppSelectorInput', ok: '继续', message: '请选择要更新的应用', parameters: paras)
}
} catch(err) {
def user = err.getCauses()[0].getUser()
if('SYSTEM' == user.toString()) {
rolloutAppSelectorTimeout = true
} else {
rolloutAppSelector = null
echo "Aborted by: [${user}]"
}
}
if (rolloutAppSelectorTimeout) {
currentBuild.result = 'FAILURE'
error "选择待更新应用超时。"
} else if (rolloutAppSelector == null) {
currentBuild.result = 'FAILURE'
error "选择待更新应用可能被用户中止了。"
} else {
echo "选择待更新应用成功。"
rolloutAppSelector.findAll{key, value -> value == true}.each { entry ->
echo "应用 ${entry} 已选择。"
}
def apps = getSelectedApp()
for (int i = 0; i < apps.size(); i++) {
def appConfig = apps[i]
appConfig.imageTag = selectedTag
}
}
}
stage ('设置选项') {
def optionsSelectorInputTimeout = false
try {
timeout(time: 180, unit: 'SECONDS') {
runOptions = input(id: 'runOptionsInput', ok: '继续', message: '请选择更新选项', parameters: [
booleanParam(defaultValue: true, description: '等待应用的每个副本都更新完成?', name: "waitEachCopy"),
booleanParam(defaultValue: true, description: '等待被依赖应用更新完成?', name: "waitDepen"),
booleanParam(defaultValue: false, description: '当镜像已存在时,强制重新导入?', name: "importImageForce")
])
}
} catch(err) {
def user = err.getCauses()[0].getUser()
if('SYSTEM' == user.toString()) {
optionsSelectorInputTimeout = true
} else {
runOptions = null
echo "选择更新选项被用户中止: [${user}]"
}
}
if (optionsSelectorInputTimeout) {
currentBuild.result = 'FAILURE'
error "选择更新选项超时"
} else if (runOptions == null) {
currentBuild.result = 'FAILURE'
error "选择更新选项错误,可能被用户中止"
} else {
waitEachCopy = runOptions.waitEachCopy.value
waitDepen = runOptions.waitDepen.value
importImageForce = runOptions.importImageForce.value
echo "成功选择更新选项"
}
}
stage ('确认') {
def apps = "";
sortAppSelectorByDependency().each { appConfig ->
wait = waitFinished(appConfig) ? "-等待" : ""
apps += "${appConfig.deploymentType}/${appConfig.deploymentName}:${appConfig.imageTag}${wait}\n"
}
def waitType = ""
if (waitDepen) {
waitType += "\n在更新前,等待被依赖的应用都更新完成。"
} else {
waitType += "\n直接更新,不等待被依赖的应用更新完成。"
}
if (waitEachCopy) {
waitType += "\n等待所有副本都更新完成"
}
else {
waitType += "\n只要有一个副本更新完成即可"
}
def message = """将更新${ocp_project}的如下应用:
${apps}
等待方式:${waitType}
目标镜像、更新顺序及等待方式如上。
强制刷新镜像: ${importImageForce}
请确认以上内容,并选择继续或中止?
"""
input (message: "将更新${ocp_project}的如下应用", ok: '确认并继续', parameters: [
string(name: 'confirmAppsToUpdate', description: message, defaultValue: "---不用输入---")]
)
}
stage ('导入镜像') {
openshift.withCluster() {
openshift.withCredentials(myToken) {
openshift.withProject(ocp_project) {
rolloutAppSelector.findAll{key, value -> value == true}.each { key, value ->
def appConfig = myConfig.find{item -> item.app == key}
stepsForImportImages["导入镜像:${appConfig.app}"] = createImportImageStep(appConfig)
}
stepsForImportImages["failFast"] = true
parallel stepsForImportImages
}
}
}
}
stage ('更新应用') {
openshift.withCluster() {
openshift.withCredentials(myToken) {
openshift.withProject(ocp_project) {
sortAppSelectorByDependency().each { appConfig ->
stepsForUpdateApp["更新应用:${appConfig.app}"] = createUpdateAppStep(appConfig)
}
parallel stepsForUpdateApp
}
}
}
}
}
def getAppGroups() {
def groups = [:]
groups.put("全部", new HashSet<String>())
for (i=0; i < myConfig.size(); i++) {
def curApp = myConfig[i]
groups["全部"].add(curApp.app)
def group = curApp.group
if (group) {
group.each {
if (!groups.containsKey(it)) {
groups.put(it, new HashSet<String>())
}
groups[it].add(curApp.app)
}
}
}
groups
}
def getSelectedAppFollowGroup() {
def selectedApps = new HashSet<String>()
rolloutGroupSelector.findAll{key, value ->
value == true
}.each { key, value ->
configs = myConfig.findAll{config -> "全部".equals(key) || (config.group && config.group.contains(key))}
if (configs) {
configs.each { config -> selectedApps.add(config.app) }
}
}
selectedApps
}
Closure createImportImageStep(appConfig) {
return {
echo "当前集群:${openshift.cluster()} 当前项目: ${openshift.project()}"
echo "登录内部镜像库..."
openshift.raw("registry login --registry=${internalRegistryAddress} --insecure=true --skip-check=true")
echo "登录外部镜像库..."
openshift.raw("registry login --registry ${externalRegistryAddress} --auth-basic=YourUsername:YourPassword --insecure=true")
echo "导入镜像到内部镜像库..."
if (!appConfig.imageTag) {
echo "${appConfig.app} 因无目标镜像跳过镜像导入"
return
}
retry(5) {
importDockerImage(appConfig)
}
}
}
Closure createUpdateAppStep(appConfig) {
return {
if (!appConfig.imageTag) {
echo "${appConfig.app} 因无目标镜像跳过更新"
appConfig.status = "skiped"
return
}
if (waitFinished(appConfig)) {
def selectedApps = getSelectedApp()
timeout (time: 60, unit: 'MINUTES') {
waitUntil(initialRecurrencePeriod: 30000, quiet: false) {
appConfig.dependencies.every {depAppName ->
def depApp = selectedApps.find{item -> item.app.equals(depAppName)}
def depFinished = depApp == null || depApp.status.equals("updated") || depApp.status.equals("skiped")
if (depApp) {
echo "${appConfig.app}依赖的${depApp.app}当前状态是:${depApp.status}"
}
depFinished
}
}
}
}
patchImage(appConfig)
if (appConfig.deploymentType == "dc") {
def latestDeploymentVersion = openshift.selector(appConfig.deploymentType, appConfig.deploymentName).object().status.latestVersion
def rc = openshift.selector('rc', "${appConfig.deploymentName}-${latestDeploymentVersion}")
timeout (time: 30, unit: 'MINUTES') {
rc.untilEach(1){
def rcMap = it.object()
def desiredReplicas = rcMap.metadata.annotations['kubectl.kubernetes.io/desired-replicas']
if (desiredReplicas == null) {
desiredReplicas = rcMap.status.replicas
}
def readyReplicas = rcMap.status.readyReplicas == null ? 0 : rcMap.status.readyReplicas
echo "desired replicas: ${desiredReplicas}, readyReplicas: ${readyReplicas}"
if (waitEachCopy) {
return desiredReplicas != null && (desiredReplicas.equals(readyReplicas))
} else {
return desiredReplicas != null && readyReplicas >=1
}
}
}
} else if (appConfig.deploymentType == "sts") {
def latestStsVersion = openshift.selector(appConfig.deploymentType, appConfig.deploymentName).object().status.updateRevision
def stsPods = openshift.selector('pods', [ 'controller-revision-hash': latestStsVersion ])
timeout (time: 30, unit: 'MINUTES') {
stsPods.untilEach(1){
if (waitEachCopy) {
return it.object().status.containerStatuses.every {
echo "pod container ready: ${it.ready}"
it.ready
}
} else {
return it.object().status.containerStatuses.any {
echo "pod container ready: ${it.ready}"
it.ready
}
}
}
}
}
appConfig.status = "updated"
echo "${appConfig.deploymentType}/${appConfig.deploymentName} rollout success."
}
}
def waitFinished(app) {
def hasDependency = app.dependencies != null && app.dependencies.size() > 0
waitDepen && hasDependency
}
def sortAppSelectorByDependency() {
def apps = getSelectedApp()
sortByDependency(apps)
}
def getSelectedApp() {
def selectedApps = []
rolloutAppSelector.findAll{key, value ->
value == true
}.each { key, value ->
config = myConfig.find{item -> item.app == key}
if (config != null) {
selectedApps.add(config)
}
}
selectedApps
}
@NonCPS
def sortByDependency(list) {
list.sort {a,b ->
b.dependencies.contains(a.app) ? -1 : a.dependencies.contains(b.app) ? 1 : 0
}
}
def patchImage(appConfig) {
def p = openshift.selector("${appConfig.deploymentType}/${appConfig.deploymentName}").object()
def originalImage = p.spec.template.spec.containers[0].image
def targetImage = "image-registry.openshift-image-registry.svc:5000/${openshift.project()}/${appConfig.imageStreamName}:${appConfig.imageTag}"
p.spec.template.spec.containers[0].image = targetImage
openshift.apply(p)
if (targetImage != originalImage) {
echo "${appConfig.deploymentType}/${appConfig.deploymentName} 镜像已由 ${originalImage} 更新为: ${openshift.project()}/${appConfig.imageStreamName}:${appConfig.imageTag},将自动触发Rollout"
} else {
echo "${appConfig.deploymentType}/${appConfig.deploymentName} 镜像未改变: ${originalImage},将强制rollout。"
openshift.selector("${appConfig.deploymentType}/${appConfig.deploymentName}").rollout().latest()
}
}
def importDockerImage(appConfig) {
echo "开始导入镜像:istag/${appConfig.imageStreamName}:${appConfig.imageTag}"
def p = openshift.selector("istag/${appConfig.imageStreamName}:${appConfig.imageTag}")
if (p.exists() && importImageForce) {
p.delete()
echo "imagestreamtag/${appConfig.imageStreamName}:${appConfig.imageTag} deleted."
}
if (!p.exists() || importImageForce) {
openshift.raw("image mirror ${externalRegistryAddress}/${appConfig.imagePath}/${appConfig.imageName}:${appConfig.imageTag} ${internalRegistryAddress}/${openshift.project()}/${appConfig.imageStreamName}:${appConfig.imageTag} --insecure=true")
}
echo "完成导入镜像:istag/${appConfig.imageStreamName}:${appConfig.imageTag}"
}
def mergeTagList(source, target) {
def result = []
if (source != null) {
result.addAll(source)
}
if (target != null) {
if (result.size() == 0) {
result.addAll(target)
} else {
def tmp = []
for (i = 0; i < target.size(); i++) {
if (result.contains(target[i])) {
tmp.add(target[i])
}
}
result = tmp
}
}
result
}
def getTagParameters() {
def parameters = []
def apps = getSelectedApp()
for (i=0; i < apps.size(); i++) {
def para = getAppTagChoiceParamter(apps[i])
if (para != null) {
parameters.add para
}
}
parameters
}
def getAppTagChoiceParamter(appConfig) {
def topTags = getLatestTag(appConfig)
if (topTags == null || topTags.size() == 0) {
return null
}
def choiceParamter = new ChoiceParameterDefinition(appConfig.app, topTags, topTags[0], "${appConfig.app}");
choiceParamter
}
def getTagChoiceParamter() {
def tags = []
tagSet.each { entry -> tags.add(entry.key) }
def sortedTags = sortReverse(tags)
def parameters = []
def para = new ChoiceParameterDefinition("latestTag", sortedTags, sortedTags[0], "最新版本")
parameters.add para
parameters
}
def getTagSet(configs) {
for (i=0; i < configs.size(); i++) {
def curApp = configs[i]
def latestTags = getLatestTag(curApp)
if (latestTags == null || latestTags.size() ==0) {
continue
}
for (int j=0; j < latestTags.size(); j++) {
def tag = latestTags[j]
if (!tagSet.containsKey(tag)) {
tagSet.put(tag, new HashSet())
}
tagSet[tag].add(curApp)
}
}
}
def getLatestTag(appConfig) {
def tags = getDockerTags(appConfig)
if (tags == null || tags.size() == 0) {
return null
}
def sortedTags = sortReverse(tags)
def topTags = sortedTags.size() > 5 ? new ArrayList(sortedTags.subList(0,4)) : sortedTags
topTags
}
def getDockerTags(appConfig) {
def url = "http://${externalRegistryAddress}/v2/${appConfig.imagePath}/${appConfig.imageName}/tags/list"
def list = getDockerImageTags(url)
list
}
@NonCPS
def sortReverse(list) {
list.sort{a,b ->
def ay = a.split("[.-]")
def by = b.split("[.-]")
for (int i = 0; i < ay.length; i++) {
def ai = ay[i].isInteger() ? ay[i].toInteger() : ay[i];
def bi = by[i].isInteger() ? by[i].toInteger() : by[i];
if (bi.compareTo(ai) == 0) {
continue
}
return bi.compareTo(ai)
}
}
}
def getDockerImageTags(url) {
def myjson = getUrl(url)
def json = jsonParse(myjson);
def tags = json.tags
tags
}
@NonCPS
def jsonParse(json) {
new groovy.json.JsonSlurperClassic().parseText(json)
}
def getUrl(url) {
sh(returnStdout: true, script: "curl -s --max-time 15 --retry 3 --retry-delay 5 ${url} 2>&1 | tee result.json")
def data = readFile('result.json').trim()
data
}
结语
本例这种方式实际为迁就部分运维同事,因部分运维同事习惯于使用WebLogic Admin Portal在界面中通过勾选操作来更新应用,故通过Jenkins Pipeline来尽量模拟这一过程。个人认为这不是一个正常的路子,还是应该通过脚本来操作,并以自动化的方式运行。