mirror of
https://github.com/appleboy/drone-jenkins.git
synced 2026-06-16 14:49:16 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51ac00b696 | |||
| f2a83d3d6c | |||
| 351ac33e2d | |||
| 984ca01afc |
@@ -46,10 +46,16 @@ Whether you're managing a hybrid CI/CD environment or orchestrating complex mult
|
|||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Jenkins Server Setup](#jenkins-server-setup)
|
- [Jenkins Server Setup](#jenkins-server-setup)
|
||||||
- [Authentication](#authentication)
|
- [Authentication](#authentication)
|
||||||
|
- [Understanding Jenkins Authentication](#understanding-jenkins-authentication)
|
||||||
|
- [CSRF Protection Notice](#csrf-protection-notice)
|
||||||
- [Parameters Reference](#parameters-reference)
|
- [Parameters Reference](#parameters-reference)
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Command Line](#command-line)
|
- [Command Line](#command-line)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Error: 403 No valid crumb was included in the request](#error-403-no-valid-crumb-was-included-in-the-request)
|
||||||
|
- [Error: 401 Unauthorized](#error-401-unauthorized)
|
||||||
|
- [Remote Token Not Working](#remote-token-not-working)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Building](#building)
|
- [Building](#building)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
@@ -125,7 +131,20 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
|
|||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
Jenkins API tokens are recommended for authentication. To create an API token:
|
#### Understanding Jenkins Authentication
|
||||||
|
|
||||||
|
Jenkins supports multiple authentication methods for triggering builds. This tool supports two approaches:
|
||||||
|
|
||||||
|
**1. API Token Authentication (Recommended)**
|
||||||
|
|
||||||
|
Use Jenkins user credentials with an API token. This method:
|
||||||
|
|
||||||
|
- ✅ Works with all Jenkins configurations
|
||||||
|
- ✅ Supports CSRF protection (enabled by default in modern Jenkins)
|
||||||
|
- ✅ Supports wait mode to monitor build completion
|
||||||
|
- ✅ Provides full access to Jenkins API features
|
||||||
|
|
||||||
|
To create an API token:
|
||||||
|
|
||||||
1. Log into Jenkins
|
1. Log into Jenkins
|
||||||
2. Click on your username (top right)
|
2. Click on your username (top right)
|
||||||
@@ -136,7 +155,42 @@ Jenkins API tokens are recommended for authentication. To create an API token:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Alternatively, you can use a remote trigger token configured in your Jenkins job settings.
|
**2. Remote Trigger Token Authentication**
|
||||||
|
|
||||||
|
Use a remote trigger token configured in your Jenkins job. **Important limitations**:
|
||||||
|
|
||||||
|
- ⚠️ **Does not work** with Jenkins CSRF protection enabled (default in modern Jenkins)
|
||||||
|
- ⚠️ Requires anonymous users to have read access to the job, OR
|
||||||
|
- ⚠️ Must be combined with API token authentication (see Combined Authentication below)
|
||||||
|
|
||||||
|
**3. Combined Authentication (Recommended for Remote Tokens)**
|
||||||
|
|
||||||
|
Use both API token and remote trigger token together:
|
||||||
|
|
||||||
|
- ✅ Works with CSRF protection enabled
|
||||||
|
- ✅ Provides double authentication security
|
||||||
|
- ✅ Supports all features including wait mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
drone-jenkins \
|
||||||
|
--host http://jenkins.example.com/ \
|
||||||
|
--user YOUR_USERNAME \
|
||||||
|
--token YOUR_API_TOKEN \
|
||||||
|
--remote-token YOUR_REMOTE_TOKEN \
|
||||||
|
--job my-jenkins-job
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CSRF Protection Notice
|
||||||
|
|
||||||
|
Modern Jenkins installations have CSRF protection enabled by default. If you encounter errors like:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Error 403 No valid crumb was included in the request
|
||||||
|
```
|
||||||
|
|
||||||
|
This means your Jenkins has CSRF protection enabled. You **must** use API token authentication (option 1 or 3 above). Remote trigger token alone will not work.
|
||||||
|
|
||||||
|
For more information about Jenkins CSRF protection, see the [official Jenkins documentation](https://www.jenkins.io/doc/book/security/csrf-protection/).
|
||||||
|
|
||||||
### Parameters Reference
|
### Parameters Reference
|
||||||
|
|
||||||
@@ -155,10 +209,19 @@ Alternatively, you can use a remote trigger token configured in your Jenkins job
|
|||||||
| Timeout | `--timeout` | `PLUGIN_TIMEOUT`, `JENKINS_TIMEOUT` | No | Maximum time to wait for job completion (default: 30m) |
|
| Timeout | `--timeout` | `PLUGIN_TIMEOUT`, `JENKINS_TIMEOUT` | No | Maximum time to wait for job completion (default: 30m) |
|
||||||
| Debug | `--debug` | `PLUGIN_DEBUG`, `JENKINS_DEBUG` | No | Enable debug mode to show detailed parameter information (default: false) |
|
| Debug | `--debug` | `PLUGIN_DEBUG`, `JENKINS_DEBUG` | No | Enable debug mode to show detailed parameter information (default: false) |
|
||||||
|
|
||||||
**Authentication Requirements**: You must provide either:
|
**Authentication Requirements**:
|
||||||
|
|
||||||
- `user` + `token` (API token authentication), OR
|
For Jenkins with **CSRF protection enabled** (default in modern Jenkins):
|
||||||
- `remote-token` (remote trigger token authentication)
|
|
||||||
|
- **Required**: `user` + `token` (API token authentication)
|
||||||
|
- **Optional**: `remote-token` (for additional security)
|
||||||
|
|
||||||
|
For Jenkins with **CSRF protection disabled** (not recommended):
|
||||||
|
|
||||||
|
- **Option 1**: `user` + `token` (API token authentication)
|
||||||
|
- **Option 2**: `remote-token` only (requires anonymous read access to job)
|
||||||
|
|
||||||
|
**Important**: If you encounter "403 No valid crumb" errors, you must use API token authentication (`user` + `token`).
|
||||||
|
|
||||||
**Parameters Format**: The `parameters` field accepts a multi-line string where each line contains one `key=value` pair:
|
**Parameters Format**: The `parameters` field accepts a multi-line string where each line contains one `key=value` pair:
|
||||||
|
|
||||||
@@ -220,9 +283,22 @@ drone-jenkins \
|
|||||||
--job my-jenkins-job
|
--job my-jenkins-job
|
||||||
```
|
```
|
||||||
|
|
||||||
**Using remote token authentication:**
|
**Using combined authentication (API token + remote token - Recommended):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
drone-jenkins \
|
||||||
|
--host http://jenkins.example.com/ \
|
||||||
|
--user appleboy \
|
||||||
|
--token XXXXXXXX \
|
||||||
|
--remote-token REMOTE_TOKEN_HERE \
|
||||||
|
--job my-jenkins-job
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using remote token only (only works without CSRF protection):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Note: This will fail if Jenkins has CSRF protection enabled
|
||||||
|
# You will get "403 No valid crumb" error
|
||||||
drone-jenkins \
|
drone-jenkins \
|
||||||
--host http://jenkins.example.com/ \
|
--host http://jenkins.example.com/ \
|
||||||
--remote-token REMOTE_TOKEN_HERE \
|
--remote-token REMOTE_TOKEN_HERE \
|
||||||
@@ -309,6 +385,18 @@ docker run --rm \
|
|||||||
ghcr.io/appleboy/drone-jenkins
|
ghcr.io/appleboy/drone-jenkins
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**With combined authentication (API token + remote token):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-e JENKINS_URL=http://jenkins.example.com/ \
|
||||||
|
-e JENKINS_USER=appleboy \
|
||||||
|
-e JENKINS_TOKEN=xxxxxxx \
|
||||||
|
-e JENKINS_REMOTE_TOKEN=your_remote_token \
|
||||||
|
-e JENKINS_JOB=my-jenkins-job \
|
||||||
|
ghcr.io/appleboy/drone-jenkins
|
||||||
|
```
|
||||||
|
|
||||||
**Wait for job completion:**
|
**Wait for job completion:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -360,6 +448,57 @@ docker run --rm \
|
|||||||
|
|
||||||
For more detailed examples and advanced configurations, see [DOCS.md](DOCS.md).
|
For more detailed examples and advanced configurations, see [DOCS.md](DOCS.md).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error: 403 No valid crumb was included in the request
|
||||||
|
|
||||||
|
**Cause**: Your Jenkins server has CSRF protection enabled (this is the default in modern Jenkins). Learn more at the [Jenkins CSRF Protection documentation](https://www.jenkins.io/doc/book/security/csrf-protection/).
|
||||||
|
|
||||||
|
**Solution**: Use API token authentication instead of remote token only:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ This will fail with CSRF protection enabled
|
||||||
|
drone-jenkins \
|
||||||
|
--host http://jenkins.example.com/ \
|
||||||
|
--remote-token YOUR_REMOTE_TOKEN \
|
||||||
|
--job my-jenkins-job
|
||||||
|
|
||||||
|
# ✅ Use this instead
|
||||||
|
drone-jenkins \
|
||||||
|
--host http://jenkins.example.com/ \
|
||||||
|
--user YOUR_USERNAME \
|
||||||
|
--token YOUR_API_TOKEN \
|
||||||
|
--job my-jenkins-job
|
||||||
|
|
||||||
|
# ✅ Or combine both for additional security
|
||||||
|
drone-jenkins \
|
||||||
|
--host http://jenkins.example.com/ \
|
||||||
|
--user YOUR_USERNAME \
|
||||||
|
--token YOUR_API_TOKEN \
|
||||||
|
--remote-token YOUR_REMOTE_TOKEN \
|
||||||
|
--job my-jenkins-job
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: 401 Unauthorized
|
||||||
|
|
||||||
|
**Cause**: Invalid credentials or incorrect authentication method.
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Verify your username and API token are correct
|
||||||
|
2. Ensure you're using an API token, not your Jenkins password
|
||||||
|
3. Check if you have permission to trigger the job
|
||||||
|
4. Make sure both `--user` and `--token` are provided together
|
||||||
|
|
||||||
|
### Remote Token Not Working
|
||||||
|
|
||||||
|
**Cause**: Remote trigger tokens alone only work in specific scenarios:
|
||||||
|
|
||||||
|
- Jenkins has CSRF protection disabled (not recommended), AND
|
||||||
|
- Anonymous users have read access to the job
|
||||||
|
|
||||||
|
**Solution**: Use combined authentication (API token + remote token) as shown in the examples above.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
- [配置](#配置)
|
- [配置](#配置)
|
||||||
- [Jenkins 服务器设置](#jenkins-服务器设置)
|
- [Jenkins 服务器设置](#jenkins-服务器设置)
|
||||||
- [认证](#认证)
|
- [认证](#认证)
|
||||||
|
- [CSRF 保护注意事项](#csrf-保护注意事项)
|
||||||
- [参数参考](#参数参考)
|
- [参数参考](#参数参考)
|
||||||
- [使用方式](#使用方式)
|
- [使用方式](#使用方式)
|
||||||
- [命令行](#命令行)
|
- [命令行](#命令行)
|
||||||
@@ -138,6 +139,18 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
|
|||||||
|
|
||||||
或者,您可以使用在 Jenkins 任务设置中配置的远程触发令牌。
|
或者,您可以使用在 Jenkins 任务设置中配置的远程触发令牌。
|
||||||
|
|
||||||
|
#### CSRF 保护注意事项
|
||||||
|
|
||||||
|
现代 Jenkins 安装默认启用 CSRF 保护。如果您遇到以下错误:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error 403 No valid crumb was included in the request
|
||||||
|
```
|
||||||
|
|
||||||
|
这表示您的 Jenkins 已启用 CSRF 保护。您**必须**使用 API 令牌认证(user + token)。单独使用远程触发令牌将无法工作。
|
||||||
|
|
||||||
|
如需更多关于 Jenkins CSRF 保护的信息,请参阅 [Jenkins 官方文档](https://www.jenkins.io/doc/book/security/csrf-protection/)。
|
||||||
|
|
||||||
### 参数参考
|
### 参数参考
|
||||||
|
|
||||||
| 参数 | CLI 标志 | 环境变量 | 必需 | 说明 |
|
| 参数 | CLI 标志 | 环境变量 | 必需 | 说明 |
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
- [設定](#設定)
|
- [設定](#設定)
|
||||||
- [Jenkins 伺服器設定](#jenkins-伺服器設定)
|
- [Jenkins 伺服器設定](#jenkins-伺服器設定)
|
||||||
- [認證](#認證)
|
- [認證](#認證)
|
||||||
|
- [CSRF 保護注意事項](#csrf-保護注意事項)
|
||||||
- [參數參考](#參數參考)
|
- [參數參考](#參數參考)
|
||||||
- [使用方式](#使用方式)
|
- [使用方式](#使用方式)
|
||||||
- [命令列](#命令列)
|
- [命令列](#命令列)
|
||||||
@@ -138,6 +139,18 @@ docker run -d -v jenkins_home:/var/jenkins_home -p 8080:8080 -p 50000:50000 --re
|
|||||||
|
|
||||||
或者,您可以使用在 Jenkins 任務設定中配置的遠端觸發令牌。
|
或者,您可以使用在 Jenkins 任務設定中配置的遠端觸發令牌。
|
||||||
|
|
||||||
|
#### CSRF 保護注意事項
|
||||||
|
|
||||||
|
現代 Jenkins 安裝預設啟用 CSRF 保護。如果您遇到以下錯誤:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error 403 No valid crumb was included in the request
|
||||||
|
```
|
||||||
|
|
||||||
|
這表示您的 Jenkins 已啟用 CSRF 保護。您**必須**使用 API 令牌認證(user + token)。單獨使用遠端觸發令牌將無法運作。
|
||||||
|
|
||||||
|
如需更多關於 Jenkins CSRF 保護的資訊,請參閱 [Jenkins 官方文件](https://www.jenkins.io/doc/book/security/csrf-protection/)。
|
||||||
|
|
||||||
### 參數參考
|
### 參數參考
|
||||||
|
|
||||||
| 參數 | CLI 旗標 | 環境變數 | 必要 | 說明 |
|
| 參數 | CLI 旗標 | 環境變數 | 必要 | 說明 |
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
module github.com/appleboy/drone-jenkins
|
module github.com/appleboy/drone-jenkins
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/appleboy/com v1.1.1
|
github.com/appleboy/com v1.2.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/urfave/cli/v2 v2.27.7
|
github.com/urfave/cli/v2 v2.27.7
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
github.com/appleboy/com v1.1.1 h1:iqu+BzrEcO3Towwi4E0GDRLSEeMBix3gf3LRjn9h8ow=
|
github.com/appleboy/com v1.2.0 h1:3jyA+yVofe/uzPHHa7Xrsj7rnDy1sZn/8pYHdzHB3GQ=
|
||||||
github.com/appleboy/com v1.1.1/go.mod h1:WKU8+CaWcyLkpm0NLhGA8Wl/yGi3KXfTIXsp7T2ceZc=
|
github.com/appleboy/com v1.2.0/go.mod h1:XK2kV+JWz/gkzsDPotNJL+aS6XCy5GNlbiTWGvIhqIU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
|||||||
+77
-13
@@ -9,6 +9,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -31,7 +32,14 @@ type (
|
|||||||
BaseURL string
|
BaseURL string
|
||||||
Token string // Remote trigger token
|
Token string // Remote trigger token
|
||||||
Client *http.Client
|
Client *http.Client
|
||||||
Debug bool // Enable debug mode to show detailed information
|
Debug bool // Enable debug mode to show detailed information
|
||||||
|
crumb *CrumbResponse // Cached CSRF crumb
|
||||||
|
}
|
||||||
|
|
||||||
|
// CrumbResponse represents Jenkins crumb issuer response for CSRF protection
|
||||||
|
CrumbResponse struct {
|
||||||
|
Crumb string `json:"crumb"`
|
||||||
|
CrumbRequestField string `json:"crumbRequestField"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueueItem represents a Jenkins queue item response
|
// QueueItem represents a Jenkins queue item response
|
||||||
@@ -144,14 +152,21 @@ func NewJenkins(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create HTTP client
|
// Create CookieJar for session management (required for CSRF crumb)
|
||||||
client := http.DefaultClient
|
jar, err := cookiejar.New(nil)
|
||||||
if tlsConfig != nil {
|
if err != nil {
|
||||||
client = &http.Client{
|
return nil, fmt.Errorf("failed to create cookie jar: %w", err)
|
||||||
Transport: &http.Transport{
|
}
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
},
|
// Create HTTP Transport with optional TLS configuration
|
||||||
}
|
transport := &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP client with CookieJar and Transport
|
||||||
|
client := &http.Client{
|
||||||
|
Jar: jar,
|
||||||
|
Transport: transport,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Jenkins{
|
return &Jenkins{
|
||||||
@@ -175,10 +190,49 @@ func (jenkins *Jenkins) buildURL(path string, params url.Values) (requestURL str
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (jenkins *Jenkins) sendRequest(req *http.Request) (*http.Response, error) {
|
// getCrumb fetches CSRF crumb from Jenkins
|
||||||
if jenkins.Auth != nil {
|
// Returns nil if CSRF protection is disabled on Jenkins
|
||||||
|
//
|
||||||
|
//nolint:unparam // Error return kept for future extensibility and API consistency
|
||||||
|
func (jenkins *Jenkins) getCrumb(ctx context.Context) (*CrumbResponse, error) {
|
||||||
|
// Return cached crumb if available
|
||||||
|
if jenkins.crumb != nil {
|
||||||
|
return jenkins.crumb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "/crumbIssuer/api/json"
|
||||||
|
var crumb CrumbResponse
|
||||||
|
err := jenkins.get(ctx, path, nil, &crumb)
|
||||||
|
if err != nil {
|
||||||
|
// CSRF protection might be disabled, log and continue
|
||||||
|
if jenkins.Debug {
|
||||||
|
log.Printf("crumb not available (CSRF may be disabled): %v", err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the crumb for subsequent requests
|
||||||
|
jenkins.crumb = &crumb
|
||||||
|
if jenkins.Debug {
|
||||||
|
log.Printf("obtained crumb: %s=%s", crumb.CrumbRequestField, crumb.Crumb)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jenkins.crumb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jenkins *Jenkins) sendRequest(
|
||||||
|
req *http.Request,
|
||||||
|
crumb *CrumbResponse,
|
||||||
|
) (*http.Response, error) {
|
||||||
|
if jenkins.Auth != nil && jenkins.Auth.Username != "" && jenkins.Auth.Token != "" {
|
||||||
req.SetBasicAuth(jenkins.Auth.Username, jenkins.Auth.Token)
|
req.SetBasicAuth(jenkins.Auth.Username, jenkins.Auth.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add CSRF crumb header if available
|
||||||
|
if crumb != nil && crumb.CrumbRequestField != "" {
|
||||||
|
req.Header.Set(crumb.CrumbRequestField, crumb.Crumb)
|
||||||
|
}
|
||||||
|
|
||||||
return jenkins.Client.Do(req)
|
return jenkins.Client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +249,7 @@ func (jenkins *Jenkins) get(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := jenkins.sendRequest(req)
|
resp, err := jenkins.sendRequest(req, nil) // GET requests don't need crumb
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -223,6 +277,16 @@ func (jenkins *Jenkins) postAndGetLocation(
|
|||||||
path string,
|
path string,
|
||||||
params url.Values,
|
params url.Values,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
|
// Fetch CSRF crumb before POST request (only if authenticated)
|
||||||
|
var crumb *CrumbResponse
|
||||||
|
if jenkins.Auth != nil && jenkins.Auth.Username != "" && jenkins.Auth.Token != "" {
|
||||||
|
var err error
|
||||||
|
crumb, err = jenkins.getCrumb(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to get crumb: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
requestURL := jenkins.buildURL(path, params)
|
requestURL := jenkins.buildURL(path, params)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, nil)
|
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, nil)
|
||||||
@@ -230,7 +294,7 @@ func (jenkins *Jenkins) postAndGetLocation(
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := jenkins.sendRequest(req)
|
resp, err := jenkins.sendRequest(req, crumb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -751,6 +751,8 @@ func TestNewJenkinsWithCACert(t *testing.T) {
|
|||||||
)
|
)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, jenkins)
|
assert.NotNil(t, jenkins)
|
||||||
assert.Equal(t, http.DefaultClient, jenkins.Client)
|
assert.NotNil(t, jenkins.Client)
|
||||||
|
// Client should have CookieJar for CSRF session management
|
||||||
|
assert.NotNil(t, jenkins.Client.Jar)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,12 +88,15 @@ func (p Plugin) validateConfig() error {
|
|||||||
if p.BaseURL == "" {
|
if p.BaseURL == "" {
|
||||||
return errors.New("jenkins base URL is required")
|
return errors.New("jenkins base URL is required")
|
||||||
}
|
}
|
||||||
if p.Username == "" {
|
|
||||||
return errors.New("jenkins username is required")
|
// Validate authentication: either (user + token) or remote-token must be provided
|
||||||
}
|
hasUserAuth := p.Username != "" && p.Token != ""
|
||||||
if p.Token == "" {
|
hasRemoteToken := p.RemoteToken != ""
|
||||||
return errors.New("jenkins API token is required")
|
|
||||||
|
if !hasUserAuth && !hasRemoteToken {
|
||||||
|
return errors.New("authentication required")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,10 +116,13 @@ func (p Plugin) Exec(ctx context.Context) error {
|
|||||||
return errors.New("at least one Jenkins job name is required")
|
return errors.New("at least one Jenkins job name is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up authentication
|
// Set up authentication (only if username and token are provided)
|
||||||
auth := &Auth{
|
var auth *Auth
|
||||||
Username: p.Username,
|
if p.Username != "" && p.Token != "" {
|
||||||
Token: p.Token,
|
auth = &Auth{
|
||||||
|
Username: p.Username,
|
||||||
|
Token: p.Token,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Jenkins client
|
// Initialize Jenkins client
|
||||||
|
|||||||
+42
-9
@@ -32,24 +32,33 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
errorMsg: "jenkins base URL is required",
|
errorMsg: "jenkins base URL is required",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing username and token",
|
name: "missing authentication",
|
||||||
plugin: Plugin{
|
plugin: Plugin{
|
||||||
BaseURL: "http://example.com",
|
BaseURL: "http://example.com",
|
||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
errorMsg: "jenkins username is required",
|
errorMsg: "authentication required",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing token",
|
name: "missing token (only username)",
|
||||||
plugin: Plugin{
|
plugin: Plugin{
|
||||||
BaseURL: "http://example.com",
|
BaseURL: "http://example.com",
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
},
|
},
|
||||||
wantError: true,
|
wantError: true,
|
||||||
errorMsg: "jenkins API token is required",
|
errorMsg: "authentication required",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "all required config present",
|
name: "missing username (only token)",
|
||||||
|
plugin: Plugin{
|
||||||
|
BaseURL: "http://example.com",
|
||||||
|
Token: "bar",
|
||||||
|
},
|
||||||
|
wantError: true,
|
||||||
|
errorMsg: "authentication required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user and token auth",
|
||||||
plugin: Plugin{
|
plugin: Plugin{
|
||||||
BaseURL: "http://example.com",
|
BaseURL: "http://example.com",
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
@@ -57,6 +66,24 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantError: false,
|
wantError: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "remote token auth",
|
||||||
|
plugin: Plugin{
|
||||||
|
BaseURL: "http://example.com",
|
||||||
|
RemoteToken: "remote-token-123",
|
||||||
|
},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both auth methods",
|
||||||
|
plugin: Plugin{
|
||||||
|
BaseURL: "http://example.com",
|
||||||
|
Username: "foo",
|
||||||
|
Token: "bar",
|
||||||
|
RemoteToken: "remote-token-123",
|
||||||
|
},
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -232,7 +259,7 @@ func TestExecMissingJenkinsUsername(t *testing.T) {
|
|||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "configuration error")
|
assert.Contains(t, err.Error(), "configuration error")
|
||||||
assert.Contains(t, err.Error(), "jenkins username is required")
|
assert.Contains(t, err.Error(), "authentication required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestExecMissingJenkinsToken tests Exec with missing token
|
// TestExecMissingJenkinsToken tests Exec with missing token
|
||||||
@@ -246,7 +273,7 @@ func TestExecMissingJenkinsToken(t *testing.T) {
|
|||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "configuration error")
|
assert.Contains(t, err.Error(), "configuration error")
|
||||||
assert.Contains(t, err.Error(), "jenkins API token is required")
|
assert.Contains(t, err.Error(), "authentication required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestExecMissingJenkinsJob tests Exec with missing or empty job list
|
// TestExecMissingJenkinsJob tests Exec with missing or empty job list
|
||||||
@@ -314,7 +341,10 @@ func TestExecTriggerMultipleJobs(t *testing.T) {
|
|||||||
// Create a mock Jenkins server
|
// Create a mock Jenkins server
|
||||||
jobsTriggered := 0
|
jobsTriggered := 0
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
jobsTriggered++
|
// Only count POST requests (job triggers), not GET requests (crumb)
|
||||||
|
if r.Method == "POST" {
|
||||||
|
jobsTriggered++
|
||||||
|
}
|
||||||
w.Header().
|
w.Header().
|
||||||
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", jobsTriggered))
|
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", jobsTriggered))
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
@@ -390,7 +420,10 @@ func TestExecWithJobsContainingWhitespace(t *testing.T) {
|
|||||||
// Create a mock Jenkins server
|
// Create a mock Jenkins server
|
||||||
jobsTriggered := 0
|
jobsTriggered := 0
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
jobsTriggered++
|
// Only count POST requests (job triggers), not GET requests (crumb)
|
||||||
|
if r.Method == "POST" {
|
||||||
|
jobsTriggered++
|
||||||
|
}
|
||||||
w.Header().
|
w.Header().
|
||||||
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", jobsTriggered))
|
Set("Location", fmt.Sprintf("http://jenkins.example.com/queue/item/%d/", jobsTriggered))
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|||||||
Reference in New Issue
Block a user