Jenkinsfile/Groovy:为什么 curl 命令会导致 "bad request"

Jenkinsfile/Groovy: Why does curl command result in "bad request"

由于对 的回答,我最近了解了 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 控制台输出的片段:

+ 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 参数。
如果我 copy/paste 那个确切的字符串变成 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 以添加硬编码 username/password,即

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

...而不是

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

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

谁能帮我找出问题所在,大概是用命令集 and/or 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(' '))
    }
   }
  }
 }
}

我尝试将您的脚本改编成管道项目:

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":
...

这个问题原来是一个字符串转义问题。工作解决方案——包括 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 环境中获得有效的解决方案字符串转义 closer/identical 到我原来的 post;但是我无法在我的 Unix/bash 环境中重现他的结果(Jenkins sh 调用在我的设置中使用 bash)。

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

值得注意:使用这个可行的解决方案,我无法再将 copy/paste curl 命令——如我的 Jenkins 作业控制台输出中所记录的那样——发送到 bash shell 并成功执行。因此,“在 Jenkins 作业控制台输出中记录的内容 完全 是 运行(即copy/pastable) 在shell."