Jenkinsfile/Groovy:为啥 curl 命令会导致“错误请求”

Posted

技术标签:

【中文标题】Jenkinsfile/Groovy:为啥 curl 命令会导致“错误请求”【英文标题】:Jenkinsfile/Groovy: Why does curl command result in "bad request"Jenkinsfile/Groovy:为什么 curl 命令会导致“错误请求” 【发布时间】:2021-10-16 06:19:28 【问题描述】:

感谢this question 的回答,我最近了解了withCredentials DSL。 尝试使用@RamKamath 的答案,即以下 Jenkinsfile:

pipeline 
agent any
stages 
stage( "1" ) 
  steps 
    script 
     def credId = "cred_id_stored_in_jenkins"
     withCredentials([usernamePassword(credentialsId: credId,
                                       passwordVariable: 'password',
                                       usernameVariable: 'username')]) 
      String url = "https://bitbucket.company.com/rest/build-status/1.0/commits"
      String commit = '0000000000000000000000000000000000000001'
      Map dict = [:]
      dict.state = "INPROGRESS"
      dict.key = "foo_002"
      dict.url = "http://server:8080/blue/organizations/jenkins/job/detail/job/002/pipeline"
      List command = []
      command.add("curl -f -L")
      command.add('-u $username:$password')
      command.add("-H \\\"Content-Type: application/json\\\"")
      command.add("-X POST $url/$commit")
      command.add("-d \\\''$JsonOutput.toJson(dict)'\\\'")
                         
      sh(script: command.join(' '))
     
    
   
  
 

...curl 命令本身因报告“错误请求”错误而失败。这是 Jenkins 控制台输出的 sn-p:

+ curl -f -L -u ****:**** -H "Content-Type:application/json" -X POST https://bitbucket.company.com/rest/build-status/1.0/commits/0000000000000000000000000000000000000001 -d '"state":"INPROGRESS","key":"foo_002","url":"http://server:8080/blue/organizations/jenkins/job/detail/job/002/pipeline"'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   153    0     0  100   153      0   4983 --:--:-- --:--:-- --:--:--  5100
curl: (22) The requested URL returned error: 400 Bad request

我知道 -u ****:****-u 的掩码 username:password 参数。 如果我将该确切的字符串复制/粘贴到 shell 中,并用实际值替换掩码值,curl 命令有效:

$ curl -f -L -u super_user:super_password -H "Content-Type:application/json" -X POST https://bitbucket.company.com/rest/build-status/1.0/commits/0000000000000000000000000000000000000001 -d '"state":"INPROGRESS","key":"foo_002","url":"http://server:8080/blue/organizations/jenkins/job/detail/job/002/pipeline"'
$ 

出了什么问题?为什么Jenkins执行curl命令会导致error 400/"Bad request",而手动执行同样的命令却运行良好?

请注意:按照建议,我将 -u $username:$password 括在单引号中,而不是双引号。


更新: 我觉得字符串插值有问题,因为如果我修改 Jenkinsfile 以添加硬编码的用户名/密码,即

command.add('-u super_user:super_password')

...而不是

command.add('-u $username:$password')

...那么curl 命令仍然像以前一样失败,即因为error: 400 Bad request

谁能帮我找出问题所在,大概是命令程序集和/或sh() 调用?


更新

我通过删除withCredentials() 简化了问题。即使是这个简化的curl 调用也会失败:

pipeline 
agent any
stages 
stage( "1" ) 
  steps 
    script 
     def credId = "cred_id_stored_in_jenkins"
     String url = "https://bitbucket.company.com/rest/build-status/1.0/commits"
     String commit = '0000000000000000000000000000000000000001'
     Map dict = [:]
     dict.state = "INPROGRESS"
     dict.key = "foo_002"
     dict.url = "http://server:8080/blue/organizations/jenkins/job/detail/job/002/pipeline"
     List command = []
     command.add("curl -f -L")
     command.add('-u super_user:super_password')
     command.add("-H \\\"Content-Type: application/json\\\"")
     command.add("-X POST $url/$commit")
     command.add("-d \\\''$JsonOutput.toJson(dict)'\\\'")

     sh(script: command.join(' '))
    
   
  
 

【问题讨论】:

command.add(...) 在 Groovy 上下文中。根据Groovy, Strings and GString literals, Double quoted string (interpolation)Groovy, String Interpolation,如果您尝试使用"..." 而不是'...' 作为字符串分隔符会怎样?如果我尝试使用单引号内联管道项目来调整您的脚本,我会得到:[curl -f -L, -u $username:$password, -H \"Content-Type: application/json\", -X POST http://localhost:8083/api/json/]. @GeroldBroser - 试一试,即现在command.add(...) 的每个实例都是command.add("...")。 IE。没有 command.add(...) 的实例使用单引号。但是curl 命令的失败与原始帖子中的相同。 @GeroldBroser - 试图将这个问题简化为最简单的 MVRE,我发现即使没有 withCredentials() 和变量,这个问题也是可重现的——即只有硬编码字符串的所有内容!我会相应地更新我的帖子;检查问题的最简单版本可能会有所帮助。 没有withCredentials()”已经是我“改编你的脚本”的一部分了。 :) @GeroldBroser - 更新时删除了 withCredentials()。我非常确定这是一些晦涩难懂的小字符串插值问题,这让我很头疼。但对于我的生活,我看不到它。试图在命令的正确位置获得双引号的字符串争论是地狱,我想我已经成功了——再次,因为如果我从 Jenkins 控制台输出中复制/粘贴命令到 shell, curl 命令有效! 【参考方案1】:

我尝试将您的脚本内嵌在管道项目中:

pipeline 
    agent any
    stages 
        stage( "1" ) 
            steps 
                script 
                  def username = '********' // my real jenkins user
                  def password = '********' // my real jenkins pwd
                  String url = "http://localhost:8083/api/json"
                  String commit = ""
                  List command = []
                  command.add("'C:/Program Files/Git/mingw64/bin/curl.exe' -f -L")
                  command.add("-u $username:$password")
                  command.add('-H "Content-Type: application/json"') // no "..." string delimiters and escaping necessary
                  //command.add("-X POST $url/$commit")
                  command.add("-X GET $url/$commit")  // GET instead
                  //command.add("-d \\\''$JsonOutput.toJson(dict)'\\\'")
            
                  sh(script: command.join(' '))
                
            
        
    

控制台输出

+ 'C:/Program Files/Git/mingw64/bin/curl.exe' -f -L -u ********:******** -H 'Content-Type: application/json' -X GET http://localhost:8083/api/json/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  3232  100  3232    0     0  42667      0 --:--:-- --:--:-- --:--:-- 43675"_class":"hudson.model.Hudson","assignedLabels":["name":"master"],"mode":"NORMAL","nodeDescription":"the master Jenkins node","nodeName":"","numExecutors":2,"description":null,"jobs":
...

我记得Jenkinsfile idiosynchrasies with escaping and quotes

与Credentials Binding Plugin

        stage( "curl withCredentials" ) 
            steps 
                script 
                    withCredentials([usernamePassword(
                            credentialsId: 'jenkins-user',
                            passwordVariable: 'password',
                            usernameVariable: 'username')]) 
                                
                      String url = "http://localhost:8083/api/json"
                      String commit = ""
                      List command = []
                      command.add("'C:/Program Files/Git/mingw64/bin/curl.exe' -f -L")
                      command.add("-u $username:$password")
                      command.add('-H "Content-Type: application/json"') // no "..." string delimiter and escaping necessary
                      //command.add("-X POST $url/$commit")
                      command.add("-X GET $url/$commit")  // GET instead
                      //command.add("-d \\\''$JsonOutput.toJson(dict)'\\\'")
    
                      sh(script: command.join(' '))
                    
                
            
                    

控制台输出

+ 'C:/Program Files/Git/mingw64/bin/curl.exe' -f -L -u ****:**** -H 'Content-Type: application/json' -X GET http://localhost:8083/api/json/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  3231  100  3231    0     0  42247      0 --:--:-- --:--:-- --:--:-- 42513
100  3231  100  3231    0     0  42216      0 --:--:-- --:--:-- --:--:-- 42513"_class":"hudson.model.Hudson","assignedLabels":["name":"master"],"mode":"NORMAL","nodeDescription":"the master Jenkins node","nodeName":"","numExecutors":2,"description":null,"jobs":
...

【讨论】:

仍然失败...我注意到您正在 Windows 上进行测试,这让我想知道这个问题是否是 Windows 插入字符串与 Linux Bash 的一些细微差别。将-v 添加到curl 命令看起来很有希望:当Jenkins 运行curl 命令时,它报告Content-Length: 143,但是当我将该命令从Jenkins 控制台输出复制/粘贴到bash shell 时,然后是curl报告Content-Length: 141。因此,Jenkins“真正”执行的内容与它在控制台日志中报告的内容之间存在一些差异。 @StoneThrow 字符串插值已经在 J​​enkins 内部完成。查看它打印为 cmd 行的内容。没有什么可以插入的了。 (顺便说一句,MinGW 和 Linux 的 Bash 在字符串插值方面并没有什么不同。我经历过的唯一区别是同一程序选项/参数的不同行为。) 天哪:猜猜curl --trace-ascii /dev/stdout ... 揭示了什么:当Jenkins 执行curl command 时,HTTP POST 内容包括一个前导和尾随单引号(json内容前后)!!!这很奇怪——我不知道如何“描述”这个问题:通过所有疯狂的转义序列,Jenkins 最终实际执行的内容包括 HTTP POST 中的前导和尾随单引号 _ content`...但是我将 Jenkins 控制台输出中打印的整个命令复制/粘贴到 Bash shell 中,它可以工作! 我现在正在检查链接。我希望这个问题特别不是字符串争吵/转义,因为以前的经验告诉我詹金斯是多么令人痛心! :P 终于!一个有效的解决方案!那是一场噩梦般的经历! :P 您的帮助非常宝贵 - 谢谢。我并不是要通过发布我自己的答案来贬低您的答案,但是我的工作解决方案与您的不同,因为我只能将其归结为 Windows 和 Unix/bash 环境之间的差异。如果您有兴趣,请查看我的答案,我希望它是全面的并提供一些很好的见解。【参考方案2】:

这个问题原来是一个字符串转义问题。可行的解决方案——包括withCredentials(),这不是问题的一个因素——对我来说是:

pipeline 
 agent any
  stages 
    stage( "1" ) 
      steps 
        script 
          def credId = "cred_id_stored_in_jenkins"
          String url = "https://bitbucket.company.com/rest/build-status/1.0/commits"
          String commit = '0000000000000000000000000000000000000001'
          withCredentials([usernamePassword(credentialsId: credId,
                                            passwordVariable: 'password',
                                            usernameVariable: 'username')]) 
            Map dict = [:]
            dict.state = "INPROGRESS"
            dict.key = "foo_002"
            dict.url = http://server:8080/blue/organizations/jenkins/job/detail/job/002/pipeline"
            def cmd = "curl -f -L" +
                      "-u $username:$password " +
                      "-H \"Content-Type: application/json\" " +
                      "-X POST $url/$commit " 
                      "-d \'$JsonOutput.toJson(dict)\'")
                         
            sh(script: cmd)
          
        
      
    
  

我确信 List.join() 的一些变体会起作用 - 没有什么特别的原因我恢复使用 + 来加入字符串,除了我正在破解之外,并确定了第一个刚刚起作用的东西.在 Jenkins 中转义字符串似乎是它自己的地狱小圈子,所以我不想花更多的时间在那里。

在做这个的过程中发现了一些奇怪的东西:

首先,Windows 与 Unix/bash 中的行为似乎有所不同:@GeroldBroser(他的帮助非常宝贵)能够在他的 Windows 环境中获得一个可行的解决方案,其中字符串转义更接近/与我的原始帖子相同;但是我无法在我的 Unix/bash 环境中重现他的结果(Jenkins sh 调用在我的设置中使用 bash)。

最后,我的印象是,记录到 Jenkins 作业控制台输出的文本字面意思是执行的 - 但这似乎并不完全正确。 总结我与@GeroldBroser 的部分评论讨论:curl 命令在 Jenkins 运行时以 error: 400 Bad request 失败,但如果我复制/粘贴/执行在 bash shell 中的 Jenkins 作业控制台输出中记录的确切 curl 命令,它会成功运行。 通过使用curl--trace-ascii /dev/stdout 选项,我发现curl 命令在bash 中成功运行时发送了141 个字节,但是当Jenkins 运行不成功时,发送了143 个字节:额外的2字节在 JSON 内容之前和之后是前导和尾随 '(单引号)字符。 这使我走上了疯狂的道路,进入了地狱之环,到达了诅咒城堡,到达了 Jenkins 逃跑的疯狂宝座,最终我得出了上述可行的解决方案。

值得注意的是:使用这个可行的解决方案,我无法再将curl 命令(在我的 Jenkins 作业控制台输出中登录)复制/粘贴到 bash shell 并成功执行。因此,“在 Jenkins 作业控制台输出中记录的内容完全是在 shell 中运行的内容(即复制/粘贴)并不是(总是)正确的。”

【讨论】:

以上是关于Jenkinsfile/Groovy:为啥 curl 命令会导致“错误请求”的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Inquire pos 在 Fortran 90 中返回 0

不明白为啥会发生 UnboundLocalError(关闭)[重复]

不明白为啥会发生 UnboundLocalError(关闭)[重复]

为啥连接的 onclick 事件不会触发?

为啥我不能将 std::string 与 libcurl 一起使用?

2021-06-18:已知数组arr,生成一个数组out,out的每个元素必须大于等于1,当arr[cur]>arr[cur-1]时,out[cur]>out[cur-1];当arr[cur]>arr