这个问题是Groovy/Jenkins的后续问题:如何重构sh(script:"curl ...")到网址?。
虽然我打算尝试使用HTTP Request
调用 REST API - 正如一位回答者所建议的那样 - 我还想专门了解/理解原始问题 RE:不可序列化性。
我减少了链接的问答中的代码,以更最低限度地演示问题.
此代码:
@Library('my-sandbox-libs@dev') sandbox_lib
pipeline {
agent any
stages {
stage( "1" ) { steps { script { echo "hello" } } }
stage( "2" ) {
steps {
script {
try {
my_lib.v4()
}
catch(Exception e) {
echo "Jenkinsfile: ${e.toString()}"
throw e
}
}
}
}
stage( "3" ) { steps { script { echo "world" } } }
}
}
// vars/my_lib.groovy
import groovy.json.JsonOutput
def v4() {
def post = new URL("https://bitbucket.company.com/rest/build-status/1.0/commits/86c36485c0cbf956a62cbc1c370f1f3eecc8665d").openConnection();
def dict = [:]
dict.state = "INPROGRESS"
dict.key = "foo_42"
dict.url = "http://url/to/nowhere"
def message = JsonOutput.toJson(dict).toString()
post.setRequestMethod("POST")
post.setDoOutput(true)
post.setRequestProperty("Content-Type", "application/json")
post.getOutputStream().write(message.getBytes("UTF-8"));
def postRC = post.getResponseCode();
println(postRC);
if (postRC.equals(200)) {
println(post.getInputStream().getText());
}
}
。生成 HTTP 错误 401。这是意料之中的,因为它无需必要的身份验证即可调用 Bitbucket REST API。
缺少的是"持有者:xyz"秘密文本。过去,我使用 Jenkins/Groovy 的withCredentials
函数获取了这个秘密文本,如下v4()
修改后的函数所示:
// vars/my_lib.groovy
import groovy.json.JsonOutput
def v4() {
def post = new URL("https://bitbucket.company.com/rest/build-status/1.0/commits/86c36485c0cbf956a62cbc1c370f1f3eecc8665d").openConnection();
def dict = [:]
dict.state = "INPROGRESS"
dict.key = "foo_42"
dict.url = "http://url/to/nowhere"
def message = JsonOutput.toJson(dict).toString()
post.setRequestMethod("POST")
post.setDoOutput(true)
post.setRequestProperty("Content-Type", "application/json")
withCredentials([string(credentialsId: 'bitbucket_cred_id',
variable: 'auth_token')]) {
post.setRequestProperty("Authorization", "Bearer " + auth_token)
}
post.getOutputStream().write(message.getBytes("UTF-8"));
def postRC = post.getResponseCode();
println(postRC);
if (postRC.equals(200)) {
println(post.getInputStream().getText());
}
}
。但我发现,特别是添加该withCredentials
块会引入java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl
运行时异常。
在链接的Stack Overflow问答中,一位评论者告诉我,问题与此函数中的可序列化对象与不可序列化对象有关。但是我很难理解使用withCredentials
块引入了哪些不可序列化的对象。
作为参考点,当我使用curl
而不是"本机"Jenkins/Groovy 函数调用相同的 REST API 时,使用相同的withCredentials
-block 工作得很好。 即以下代码工作正常:
def v1() {
def dict = [:]
dict.state = "INPROGRESS"
dict.key = "foo_42"
dict.url = "http://url/to/nowhere"
withCredentials([string(credentialsId: 'bitbucket_cred_id',
variable: 'auth_token')]) {
def cmd = "curl -f -L " +
"-H "Authorization: Bearer ${auth_token}" " +
"-H "Content-Type:application/json" " +
"-X POST https://bitbucket.company.com/rest/build-status/1.0/commits/86c36485c0cbf956a62cbc1c370f1f3eecc8665d " +
"-d '${JsonOutput.toJson(dict)}'"
sh(script: cmd, returnStatus: true)
}
}
所以,总而言之,这个问题是为什么withCredentials()
引入不可序列化的对象(以及什么是不可序列化的对象),这会导致使用URL
和/或HttpURLConnection
时出现NonSerializableException
异常,并且有人解决这个问题吗?
我不是不愿意使用不同的解决方案,例如httpRequest
对象,但这个问题是关于学习这个特定问题的本质,以及如何使用现有对象来解决它,即URL
和HttpURLConnection
对象。
Update:事实证明,我无法使用httpRequest
对象使用建议的替代解决方案,因为HTTP Request
插件仅适用于 Jenkins 版本 2.222.4 或更高版本,我们的 Jenkins 不符合。更新我们的 Jenkins 版本超出了我的特权,基本上我需要假设无法升级 Jenkins。
我们的 Jenkins 版本是 2.190.3,它有 Groovy 版本 2.4.12。
当遇到此类问题时,我通常会尝试将调用Groovy/Java API的"低级"代码放入@NonCPS
函数中。这些函数中的对象不需要可序列化,因此我们可以自由使用任何Groovy/Java API。
背景阅读:管道 CPS 方法不匹配
确保你没有从@NonCPS
函数(或任何其他未标记为@NonCPS
的函数)调用任何 Jenkins 管道步骤——这样的代码可能会默默失败或做意外的事情!(不过有一些"安全"功能,比如echo
。
由于withCredentials
是一个管道步骤,因此必须从"常规"函数(未标记为@NonCPS
)调用它,该函数在调用链上一级。
请注意,我将auth_token
作为参数传递给v4_internal
。如果你在代码中需要其他 Jenkins 变量,这些变量也应该作为参数传递。
// vars/my_lib.groovy
import groovy.json.JsonOutput
def v4() {
withCredentials([string(credentialsId: 'bitbucket_cred_id',
variable: 'auth_token')]) {
v4_internal(auth_token)
}
}
@NonCPS
def v4_internal( def auth_token ) {
def post = new URL("https://bitbucket.company.com/rest/build-status/1.0/commits/86c36485c0cbf956a62cbc1c370f1f3eecc8665d").openConnection();
def dict = [:]
dict.state = "INPROGRESS"
dict.key = "foo_42"
dict.url = "http://url/to/nowhere"
def message = JsonOutput.toJson(dict).toString()
post.setRequestMethod("POST")
post.setDoOutput(true)
post.setRequestProperty("Content-Type", "application/json")
post.setRequestProperty("Authorization", "Bearer " + auth_token)
post.getOutputStream().write(message.getBytes("UTF-8"));
def postRC = post.getResponseCode();
println(postRC);
if (postRC.equals(200)) {
println(post.getInputStream().getText());
}
}
这很糟糕 - 我希望有人能给出更好的答案 - 但看起来我能做到这一点的唯一方法如下:
@Library('my-sandbox-libs@dev') sandbox_lib
pipeline {
agent any
stages {
stage( "1" ) { steps { script { echo "hello" } } }
stage( "2" ) {
steps {
script {
try {
my_lib.v5(my_lib.getBitbucketCred())
}
catch(Exception e) {
echo "Jenkinsfile: ${e.toString()}"
throw e
}
}
}
}
stage( "3" ) { steps { script { echo "world" } } }
}
}
// vars/my_lib.groovy
import groovy.json.JsonOutput
def getBitbucketCred() {
withCredentials([string(credentialsId: 'bitbucket_cred_id',
variable: 'auth_token')]) {
return auth_token
}
}
def v5(auth_token) {
def post = new URL("https://bitbucket.company.com/rest/build-status/1.0/commits/86c36485c0cbf956a62cbc1c370f1f3eecc8665d").openConnection();
def dict = [:]
dict.state = "INPROGRESS"
dict.key = "foo_42"
dict.url = "http://url/to/nowhere"
def message = JsonOutput.toJson(dict).toString()
post.setRequestMethod("POST")
post.setDoOutput(true)
post.setRequestProperty("Content-Type", "application/json")
req.setRequestProperty("Authorization", "Bearer " + auth_token)
post.getOutputStream().write(message.getBytes("UTF-8"));
def postRC = post.getResponseCode();
println(postRC);
if (postRC.equals(200)) {
println(post.getInputStream().getText());
}
}
具体来说,我必须调用与使用URL
和/或HttpURLConnection
的函数的范围完全分开withCredentials()
。
我不知道这在 Jenkins/Groovy 中是否被认为是可以接受的,但我对无法从v5()
函数本身调用withCredentials()
感到不满。我也无法从v5()
调用withCredentials()
包装器函数。
当我尝试直接在v5()
或从v5()
调用的包装器函数调用withCredentials()
时,我尝试了v5()
和包装器函数之间的所有@NonCPS
组合,但没有奏效。我还尝试在函数结束之前显式地将URL
和HttpURLConnection
对象设置为null
(如Jenkins/Groovy:为什么withCredentials()引入NotSerializableException异常?),但这也没有奏效。
如果这是唯一的解决方案,我会对 Jenkins/Groovy 感到失望。这感觉就像是对如何选择组织他的代码的人为限制。
更新了更多详细信息以响应@daggett:
RE:直接从my_lib.v5()
调用withCredentials()
或从包装函数调用它,让我们从mylib.groovy
设置开始,如下所示(让我也借此机会给函数起更好的名字):
def withCredWrapper() {
withCredentials([string(credentialsId: 'bitbucket_cred_id',
variable: 'auth_token')]) {
return auth_token
}
}
def callRestFunc() {
def post = new URL("https://bitbucket.company.com/rest/build-status/1.0/commits/86c36485c0cbf956a62cbc1c370f1f3eecc8665d").openConnection();
def dict = [:]
dict.state = "INPROGRESS"
dict.key = "foo_42"
dict.url = "http://url/to/nowhere"
def message = JsonOutput.toJson(dict).toString()
post.setRequestMethod("POST")
post.setDoOutput(true)
post.setRequestProperty("Content-Type", "application/json")
// version 1:
withCredentials([string(credentialsId: 'bb_auth_bearer_token_cred_id',
variable: 'auth_token')]) {
post.setRequestProperty("Authorization", "Bearer " + auth_token)
}
// version 2:
//post.setRequestProperty("Authorization", "Bearer " + withCredWrapper())
post.getOutputStream().write(message.getBytes("UTF-8"));
def postRC = post.getResponseCode();
println(postRC);
if (postRC.equals(200)) {
println(post.getInputStream().getText());
}
}
使用上面的代码,函数callRestFunc()
可以直接调用withCredentials()
,如上所述,也可以通过包装函数withCredWrapper()
间接调用,即:
...
// version 1:
//withCredentials([string(credentialsId: 'bb_auth_bearer_token_cred_id',
// variable: 'auth_token')]) {
// post.setRequestProperty("Authorization", "Bearer " + auth_token)
//}
// version 2:
post.setRequestProperty("Authorization", "Bearer " + withCredWrapper())
...
此外,@NonCPS
可以应用于withCredWrapper()
或callRestFunc()
之一,两者兼而有之,或两者都不应用于。
以下是所有 8 种组合的具体故障:
1.
def withCredWrapper() {
...
}
def callRestFunc() {
...
// version 1:
withCredentials(...)
...
}
失败:Jenkinsfile: java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl
阿拉伯数字。
def withCredWrapper() {
...
}
@NonCPS
def callRestFunc() {
...
// version 1:
withCredentials(...)
...
}
故障:expected to call my_lib.callRestFunc but wound up catching withCredentials; see: https://jenkins.io/redirect/pipeline-cps-method-mismatches/ Masking supported pattern matches of $auth_token
,Jenkinsfile: java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl
3.
@NonCPS
def withCredWrapper() {
...
}
def callRestFunc() {
...
// version 1:
withCredentials(...)
...
}
失败:Jenkinsfile: java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl
4.
@NonCPS
def withCredWrapper() {
...
}
@NonCPS
def callRestFunc() {
...
// version 1:
withCredentials(...)
...
}
故障:expected to call my_lib.callRestFunc but wound up catching withCredentials; see: https://jenkins.io/redirect/pipeline-cps-method-mismatches/ Masking supported pattern matches of $auth_token
,Jenkinsfile: java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl
5.
def withCredWrapper() {
...
}
def callRestFunc() {
...
// version 2:
post.setRequestProperty("Authorization", "Bearer " + withCredWrapper())
...
}
失败:Jenkinsfile: java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl
6.
def withCredWrapper() {
...
}
@NonCPS
def callRestFunc() {
...
// version 2:
post.setRequestProperty("Authorization", "Bearer " + withCredWrapper())
...
}
失败:expected to call my_lib.callRestFunc but wound up catching my_lib.withCredWrapper; see: https://jenkins.io/redirect/pipeline-cps-method-mismatches/
7.
@NonCPS
def withCredWrapper() {
...
}
def callRestFunc() {
...
// version 2:
post.setRequestProperty("Authorization", "Bearer " + withCredWrapper())
...
}
故障:expected to call my_lib.withCredWrapper but wound up catching withCredentials; see: https://jenkins.io/redirect/pipeline-cps-method-mismatches/ Masking supported pattern matches of $auth_token
,Jenkinsfile: java.io.NotSerializableException: sun.net.www.protocol.https.HttpsURLConnectionImpl
8.
@NonCPS
def withCredWrapper() {
...
}
@NonCPS
def callRestFunc() {
...
// version 2:
post.setRequestProperty("Authorization", "Bearer " + withCredWrapper())
...
}
失败:expected to call my_lib.callRestFunc but wound up catching withCredentials; see: https://jenkins.io/redirect/pipeline-cps-method-mismatches/