Compare commits

..

5 Commits

Author SHA1 Message Date
Bo-Yi Wu 02829360ad feat: add support for custom CA certificates for SSL/TLS connections (#42)
* feat: add support for custom CA certificates for SSL/TLS connections

- Add support for custom CA certificates (via PEM content, file path, or HTTP/HTTPS URL) for SSL/TLS connections.
- Document the new CA certificate option and usage examples for CLI, Docker, and Drone CI in the README.
- Update Jenkins client initialization to load and validate a custom CA certificate if provided, using a priority where insecure mode overrides custom CA.
- Introduce comprehensive tests for CA certificate loading and Jenkins client initialization with different CA certificate sources and error scenarios.
- Register the new ca-cert command-line flag and propagate its value through configuration and debug output.
- Ensure that error handling for certificate loading fully propagates failures.

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* test: update test CA certificate with new sample

- Replace the sample CA certificate used in tests with a new certificate

Signed-off-by: appleboy <appleboy.tw@gmail.com>

---------

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-06 13:11:29 +08:00
appleboy f3a67c62a6 chore: update third-party libraries to latest compatible versions
- Update urfave/cli library to version 2.27.7
- Update xrash/smetrics library to a newer commit

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-06 10:58:35 +08:00
Bo-Yi Wu 069e6455cc ci: update build workflow and dependencies for Go 1.24 compatibility
- Update Go version requirement from 1.22 to 1.24.0
- Add github.com/appleboy/com as a dependency
- Set GitHub Actions output with build result and URL after Jenkins build completion
- Log a warning if setting GitHub output fails

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 19:42:44 +08:00
Bo-Yi Wu eb51e55e81 build: enable detailed build info logging in debug mode
- Add debug logging to display final build information when debug mode is enabled

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 19:40:30 +08:00
Bo-Yi Wu da87ddb86b docs: document and illustrate debug mode configuration options
- Document the new debug mode, including its purpose and usage
- Add examples showing how to enable debug mode via CLI, Docker, and YAML configuration
- Update the configuration table to describe the debug option and its environment variables

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-12-02 17:25:40 +08:00
7 changed files with 470 additions and 27 deletions
+101 -1
View File
@@ -40,7 +40,8 @@ A [Drone](https://github.com/drone/drone) plugin for triggering [Jenkins](https:
- Support for Jenkins build parameters
- Multiple authentication methods (API token or remote trigger token)
- Wait for job completion with configurable polling and timeout
- SSL/TLS support with optional insecure mode
- Debug mode with detailed parameter information and secure token masking
- SSL/TLS support with custom CA certificates (PEM content, file path, or URL)
- Cross-platform support (Linux, macOS, Windows)
- Available as binary, Docker image, or Drone plugin
@@ -126,9 +127,11 @@ Alternatively, you can use a remote trigger token configured in your Jenkins job
| Job | `--job`, `-j` | `PLUGIN_JOB`, `JENKINS_JOB` | Yes | Jenkins job name(s) - can specify multiple |
| Parameters | `--parameters`, `-p` | `PLUGIN_PARAMETERS`, `JENKINS_PARAMETERS` | No | Build parameters in multi-line `key=value` format (one per line) |
| Insecure | `--insecure` | `PLUGIN_INSECURE`, `JENKINS_INSECURE` | No | Allow insecure SSL connections (default: false) |
| CA Cert | `--ca-cert` | `PLUGIN_CA_CERT`, `JENKINS_CA_CERT` | No | Custom CA certificate (PEM content, file path, or HTTP URL) |
| Wait | `--wait` | `PLUGIN_WAIT`, `JENKINS_WAIT` | No | Wait for job completion (default: false) |
| Poll Interval | `--poll-interval` | `PLUGIN_POLL_INTERVAL`, `JENKINS_POLL_INTERVAL` | No | Interval between status checks (default: 10s) |
| 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) |
**Authentication Requirements**: You must provide either:
@@ -217,6 +220,37 @@ drone-jenkins \
--timeout 1h
```
**With debug mode:**
```bash
drone-jenkins \
--host http://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--debug
```
**With custom CA certificate:**
```bash
# Using a file path
drone-jenkins \
--host https://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--ca-cert /path/to/ca.pem
# Using a URL
drone-jenkins \
--host https://jenkins.example.com/ \
--user appleboy \
--token XXXXXXXX \
--job my-jenkins-job \
--ca-cert https://example.com/ca-bundle.crt
```
### Docker
**Single job:**
@@ -267,6 +301,41 @@ docker run --rm \
ghcr.io/appleboy/drone-jenkins
```
**With debug mode:**
```bash
docker run --rm \
-e JENKINS_URL=http://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_DEBUG=true \
ghcr.io/appleboy/drone-jenkins
```
**With custom CA certificate:**
```bash
# Using a mounted certificate file
docker run --rm \
-v /path/to/ca.pem:/ca.pem:ro \
-e JENKINS_URL=https://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_CA_CERT=/ca.pem \
ghcr.io/appleboy/drone-jenkins
# Using a URL
docker run --rm \
-e JENKINS_URL=https://jenkins.example.com/ \
-e JENKINS_USER=appleboy \
-e JENKINS_TOKEN=xxxxxxx \
-e JENKINS_JOB=my-jenkins-job \
-e JENKINS_CA_CERT=https://example.com/ca-bundle.crt \
ghcr.io/appleboy/drone-jenkins
```
### Drone CI
Add the plugin to your `.drone.yml`:
@@ -337,6 +406,37 @@ steps:
timeout: 1h
```
**With debug mode:**
```yaml
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: http://jenkins.example.com/
user: appleboy
token:
from_secret: jenkins_token
job: my-jenkins-job
debug: true
```
**With custom CA certificate:**
```yaml
steps:
- name: trigger-jenkins
image: ghcr.io/appleboy/drone-jenkins
settings:
url: https://jenkins.example.com/
user: appleboy
token:
from_secret: jenkins_token
job: my-jenkins-job
ca_cert:
from_secret: jenkins_ca_cert
```
For more detailed examples and advanced configurations, see [DOCS.md](DOCS.md).
## Development
+4 -3
View File
@@ -1,11 +1,12 @@
module github.com/appleboy/drone-jenkins
go 1.22
go 1.24.0
require (
github.com/appleboy/com v1.1.1
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.5
github.com/urfave/cli/v2 v2.27.7
github.com/yassinebenaid/godump v0.11.1
)
@@ -14,6 +15,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+6 -4
View File
@@ -1,3 +1,5 @@
github.com/appleboy/com v1.1.1 h1:iqu+BzrEcO3Towwi4E0GDRLSEeMBix3gf3LRjn9h8ow=
github.com/appleboy/com v1.1.1/go.mod h1:WKU8+CaWcyLkpm0NLhGA8Wl/yGi3KXfTIXsp7T2ceZc=
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -10,10 +12,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI=
github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+110 -8
View File
@@ -3,15 +3,18 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/appleboy/com/gh"
"github.com/yassinebenaid/godump"
)
@@ -55,27 +58,108 @@ type (
}
)
// NewJenkins is initial Jenkins object
func NewJenkins(auth *Auth, url string, token string, insecure bool, debug bool) *Jenkins {
url = strings.TrimRight(url, "/")
// loadCACert loads a CA certificate from various sources:
// - PEM content (if it starts with "-----BEGIN")
// - File path (if the file exists)
// - HTTP/HTTPS URL (if it starts with "http://" or "https://")
func loadCACert(caCert string) ([]byte, error) {
if caCert == "" {
return nil, nil
}
client := http.DefaultClient
// Check if it's PEM content (starts with BEGIN marker)
if strings.HasPrefix(strings.TrimSpace(caCert), "-----BEGIN") {
return []byte(caCert), nil
}
// Check if it's an HTTP/HTTPS URL
if strings.HasPrefix(caCert, "http://") || strings.HasPrefix(caCert, "https://") {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, caCert, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for CA certificate URL: %w", err)
}
resp, err := http.DefaultClient.Do(req) // #nosec G107 -- URL is user-provided configuration
if err != nil {
return nil, fmt.Errorf("failed to fetch CA certificate from URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch CA certificate: HTTP %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate from URL: %w", err)
}
return data, nil
}
// Otherwise, treat it as a file path
data, err := os.ReadFile(caCert)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate file: %w", err)
}
return data, nil
}
// NewJenkins is initial Jenkins object
func NewJenkins(
auth *Auth,
baseURL string,
token string,
insecure bool,
caCert string,
debug bool,
) (*Jenkins, error) {
baseURL = strings.TrimRight(baseURL, "/")
// Load CA certificate if provided
caCertData, err := loadCACert(caCert)
if err != nil {
return nil, fmt.Errorf("failed to load CA certificate: %w", err)
}
// Build TLS configuration
var tlsConfig *tls.Config
if insecure {
// #nosec G402 -- InsecureSkipVerify is intentionally configurable by user
tlsConfig = &tls.Config{InsecureSkipVerify: true}
} else if caCertData != nil {
// Create certificate pool with custom CA
certPool, err := x509.SystemCertPool()
if err != nil {
// Fall back to empty pool if system pool unavailable
certPool = x509.NewCertPool()
}
if !certPool.AppendCertsFromPEM(caCertData) {
return nil, fmt.Errorf("failed to parse CA certificate")
}
tlsConfig = &tls.Config{
RootCAs: certPool,
MinVersion: tls.VersionTLS12,
}
}
// Create HTTP client
client := http.DefaultClient
if tlsConfig != nil {
client = &http.Client{
Transport: &http.Transport{
// #nosec G402 -- InsecureSkipVerify is intentionally configurable by user
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSClientConfig: tlsConfig,
},
}
}
return &Jenkins{
Auth: auth,
BaseURL: url,
BaseURL: baseURL,
Token: token,
Client: client,
Debug: debug,
}
}, nil
}
func (jenkins *Jenkins) buildURL(path string, params url.Values) (requestURL string) {
@@ -293,6 +377,24 @@ func (jenkins *Jenkins) waitForCompletion(
buildNumber,
buildInfo.Result,
)
// Debug: Display final build info
if jenkins.Debug {
log.Println("=== Debug Mode: Build Result ===")
if err := godump.Dump(buildInfo); err != nil {
log.Printf("warning: failed to dump build info: %v", err)
}
log.Println("================================")
}
// Set GitHub Actions output
if err := gh.SetOutput(map[string]string{
"result": buildInfo.Result,
"url": buildInfo.URL,
}); err != nil {
log.Printf("warning: failed to set GitHub output: %v", err)
}
return buildInfo, nil
}
+236 -10
View File
@@ -4,6 +4,8 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
@@ -16,7 +18,8 @@ func TestParseJobPath(t *testing.T) {
Username: "appleboy",
Token: "1234",
}
jenkins := NewJenkins(auth, "http://example.com", "", false, false)
jenkins, err := NewJenkins(auth, "http://example.com", "", false, "", false)
assert.NoError(t, err)
assert.Equal(t, "/job/foo", jenkins.parseJobPath("/foo/"))
assert.Equal(t, "/job/foo", jenkins.parseJobPath("foo/"))
@@ -29,7 +32,8 @@ func TestUnSupportProtocol(t *testing.T) {
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, "example.com", "", false, false)
jenkins, err := NewJenkins(auth, "example.com", "", false, "", false)
assert.NoError(t, err)
queueID, err := jenkins.trigger("drone-jenkins", nil)
assert.NotNil(t, err)
@@ -50,7 +54,8 @@ func TestTriggerBuild(t *testing.T) {
Username: "foo",
Token: "bar",
}
jenkins := NewJenkins(auth, server.URL, "remote-token", false, false)
jenkins, err := NewJenkins(auth, server.URL, "remote-token", false, "", false)
assert.NoError(t, err)
params := url.Values{"param": []string{"value"}}
queueID, err := jenkins.trigger("drone-jenkins", params)
@@ -110,7 +115,8 @@ func TestPostAndGetLocation(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false, false)
jenkins, err := NewJenkins(auth, server.URL, "", false, "", false)
assert.NoError(t, err)
queueID, err := jenkins.postAndGetLocation("/test", nil)
@@ -186,7 +192,8 @@ func TestGetQueueItem(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false, false)
jenkins, err := NewJenkins(auth, server.URL, "", false, "", false)
assert.NoError(t, err)
queueItem, err := jenkins.getQueueItem(tt.queueID)
@@ -274,7 +281,8 @@ func TestGetBuildInfo(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false, false)
jenkins, err := NewJenkins(auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.getBuildInfo(tt.jobName, tt.buildNumber)
@@ -332,7 +340,8 @@ func TestWaitForCompletion(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false, false)
jenkins, err := NewJenkins(auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.waitForCompletion(
"test-job",
@@ -364,7 +373,8 @@ func TestWaitForCompletion(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false, false)
jenkins, err := NewJenkins(auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.waitForCompletion(
"test-job",
@@ -404,7 +414,8 @@ func TestWaitForCompletion(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false, false)
jenkins, err := NewJenkins(auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.waitForCompletion(
"test-job",
@@ -449,7 +460,8 @@ func TestWaitForCompletion(t *testing.T) {
Username: "test",
Token: "test",
}
jenkins := NewJenkins(auth, server.URL, "", false, false)
jenkins, err := NewJenkins(auth, server.URL, "", false, "", false)
assert.NoError(t, err)
buildInfo, err := jenkins.waitForCompletion(
"test-job",
@@ -465,3 +477,217 @@ func TestWaitForCompletion(t *testing.T) {
assert.Equal(t, "FAILURE", buildInfo.Result)
})
}
// Sample CA certificate for testing (self-signed, not for production use)
const testCACert = `-----BEGIN CERTIFICATE-----
MIIDAzCCAeugAwIBAgIUGYBGBr+t20UAWJorEPULxzGIXUEwDQYJKoZIhvcNAQEL
BQAwETEPMA0GA1UEAwwGdGVzdGNhMB4XDTI1MTIwNjA1MDgzMloXDTM1MTIwNDA1
MDgzMlowETEPMA0GA1UEAwwGdGVzdGNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAq4bwnABqFenRVUoHLKhPiJXkh6TBFUaCWiEpKYNPywptBJNdyWNf
ouDxJ8gvQOMCkp3trnAHFcT6W5s8QLM1Hf/70QZI9GU/BtYm0KijU8aM+GJawNto
sK103TeCd0tVenDkxfamBGYnh3L5jtk0V/jeIsAIfFoe9Citu3MttRfxnSmZ4w2K
qlS14vKhFlO4WrXAh9j4PaVE5DL7jya/UKe6VVQIONCwUipRN6nU3UXhR7akVSmF
/bYkFsfdcErXJHjDpg+0xOsa5LJhzRkx5Uoqtviq2oRVVYhZc0eTwjq/407ocJ25
6WmerfKrtFDpzOZPa4XPVX9Am4vWugtrwQIDAQABo1MwUTAdBgNVHQ4EFgQUh7kL
LqmsvQP3TI6eiLVK7Gs7A00wHwYDVR0jBBgwFoAUh7kLLqmsvQP3TI6eiLVK7Gs7
A00wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApLOdWacya+Zi
w0Fd3UfSveuRsayAkMkZ4p0L9XKlADzwKtSF1Ykn6wiEiYfXd2TvffsR2XglOXFc
181IpBhP5u2mzK6pRvH9mqTs3w8JTcXMFmg8AKE2Vg5k22tBM2OUJJgKXkiACuHS
pZeOOvJcnjGunbTRwqais0TLYnkOcFsbgrSBKv82HiVootH/iKZahf1ViFMOURTh
MqjwIous7Y53Rq4RmfycIjNwODlDW0i5atKe8incDBiIYKw6sH8WN+nuhnHC/vJ5
5ZQvGCUsGOvma5ojWAiLs8wu4dODuF5ZNID3t+M36PQs7JDaQNN+AkZROOTSMqa/
ud3vS1A5+g==
-----END CERTIFICATE-----`
func TestLoadCACert(t *testing.T) {
t.Run("empty string returns nil", func(t *testing.T) {
data, err := loadCACert("")
assert.NoError(t, err)
assert.Nil(t, data)
})
t.Run("PEM content directly", func(t *testing.T) {
data, err := loadCACert(testCACert)
assert.NoError(t, err)
assert.NotNil(t, data)
assert.Contains(t, string(data), "BEGIN CERTIFICATE")
})
t.Run("PEM content with leading whitespace", func(t *testing.T) {
data, err := loadCACert(" \n" + testCACert)
assert.NoError(t, err)
assert.NotNil(t, data)
assert.Contains(t, string(data), "BEGIN CERTIFICATE")
})
t.Run("file path", func(t *testing.T) {
// Create a temporary file with the certificate
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "ca.pem")
err := os.WriteFile(certFile, []byte(testCACert), 0o600)
assert.NoError(t, err)
data, err := loadCACert(certFile)
assert.NoError(t, err)
assert.NotNil(t, data)
assert.Contains(t, string(data), "BEGIN CERTIFICATE")
})
t.Run("file not found", func(t *testing.T) {
data, err := loadCACert("/nonexistent/path/ca.pem")
assert.Error(t, err)
assert.Nil(t, data)
assert.Contains(t, err.Error(), "failed to read CA certificate file")
})
t.Run("HTTP URL", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(testCACert))
}))
defer server.Close()
data, err := loadCACert(server.URL)
assert.NoError(t, err)
assert.NotNil(t, data)
assert.Contains(t, string(data), "BEGIN CERTIFICATE")
})
t.Run("HTTPS URL", func(t *testing.T) {
server := httptest.NewTLSServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(testCACert))
}),
)
defer server.Close()
// Note: This test uses the test server's self-signed cert
// In real scenarios, the URL would be to a trusted source
// We skip HTTPS verification for this test
data, err := loadCACert(server.URL)
// This may fail due to certificate verification, which is expected
if err != nil {
assert.Contains(t, err.Error(), "certificate")
} else {
assert.NotNil(t, data)
}
})
t.Run("HTTP URL returns error status", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
data, err := loadCACert(server.URL)
assert.Error(t, err)
assert.Nil(t, data)
assert.Contains(t, err.Error(), "HTTP 404")
})
t.Run("HTTP URL unreachable", func(t *testing.T) {
data, err := loadCACert("http://localhost:59999/nonexistent")
assert.Error(t, err)
assert.Nil(t, data)
assert.Contains(t, err.Error(), "failed to fetch CA certificate from URL")
})
}
func TestNewJenkinsWithCACert(t *testing.T) {
t.Run("with valid CA certificate", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(auth, "https://example.com", "", false, testCACert, false)
assert.NoError(t, err)
assert.NotNil(t, jenkins)
assert.NotNil(t, jenkins.Client)
})
t.Run("with CA certificate from file", func(t *testing.T) {
tmpDir := t.TempDir()
certFile := filepath.Join(tmpDir, "ca.pem")
err := os.WriteFile(certFile, []byte(testCACert), 0o600)
assert.NoError(t, err)
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(auth, "https://example.com", "", false, certFile, false)
assert.NoError(t, err)
assert.NotNil(t, jenkins)
})
t.Run("with invalid CA certificate content", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(
auth,
"https://example.com",
"",
false,
"invalid-cert-data",
false,
)
assert.Error(t, err)
assert.Nil(t, jenkins)
assert.Contains(t, err.Error(), "failed to read CA certificate file")
})
t.Run("with invalid PEM format", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
invalidPEM := "-----BEGIN CERTIFICATE-----\ninvalid-base64-data\n-----END CERTIFICATE-----"
jenkins, err := NewJenkins(auth, "https://example.com", "", false, invalidPEM, false)
assert.Error(t, err)
assert.Nil(t, jenkins)
assert.Contains(t, err.Error(), "failed to parse CA certificate")
})
t.Run("with nonexistent file path", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(
auth,
"https://example.com",
"",
false,
"/nonexistent/ca.pem",
false,
)
assert.Error(t, err)
assert.Nil(t, jenkins)
assert.Contains(t, err.Error(), "failed to load CA certificate")
})
t.Run("insecure flag takes precedence over CA cert", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
// When insecure is true, CA cert should be ignored
jenkins, err := NewJenkins(auth, "https://example.com", "", true, testCACert, false)
assert.NoError(t, err)
assert.NotNil(t, jenkins)
})
t.Run("without CA certificate uses default client", func(t *testing.T) {
auth := &Auth{
Username: "test",
Token: "test",
}
jenkins, err := NewJenkins(auth, "https://example.com", "", false, "", false)
assert.NoError(t, err)
assert.NotNil(t, jenkins)
assert.Equal(t, http.DefaultClient, jenkins.Client)
})
}
+8
View File
@@ -92,6 +92,11 @@ func main() {
Usage: "allow insecure server connections when using SSL",
EnvVars: []string{"PLUGIN_INSECURE", "JENKINS_INSECURE", "INPUT_INSECURE"},
},
&cli.StringFlag{
Name: "ca-cert",
Usage: "custom CA certificate (PEM content, file path, or HTTP URL)",
EnvVars: []string{"PLUGIN_CA_CERT", "JENKINS_CA_CERT", "INPUT_CA_CERT"},
},
&cli.StringFlag{
Name: "parameters",
Aliases: []string{"p"},
@@ -184,6 +189,7 @@ func run(c *cli.Context) error {
RemoteToken: c.String("remote-token"),
Job: c.StringSlice("job"),
Insecure: c.Bool("insecure"),
CACert: c.String("ca-cert"),
Parameters: c.String("parameters"),
Wait: c.Bool("wait"),
PollInterval: c.Duration("poll-interval"),
@@ -203,6 +209,7 @@ func run(c *cli.Context) error {
RemoteToken string
Job []string
Insecure bool
CACert string
Parameters string
Wait bool
PollInterval time.Duration
@@ -215,6 +222,7 @@ func run(c *cli.Context) error {
RemoteToken: maskToken(plugin.RemoteToken),
Job: plugin.Job,
Insecure: plugin.Insecure,
CACert: plugin.CACert,
Parameters: plugin.Parameters,
Wait: plugin.Wait,
PollInterval: plugin.PollInterval,
+5 -1
View File
@@ -19,6 +19,7 @@ type (
RemoteToken string // Optional remote trigger token for additional security
Job []string // List of Jenkins job names to trigger
Insecure bool // Whether to skip TLS certificate verification
CACert string // Custom CA certificate (PEM content, file path, or HTTP URL)
Parameters string // Job parameters in key=value format (one per line)
Wait bool // Whether to wait for job completion
PollInterval time.Duration // Interval between status checks (default: 10s)
@@ -117,7 +118,10 @@ func (p Plugin) Exec() error {
}
// Initialize Jenkins client
jenkins := NewJenkins(auth, p.BaseURL, p.RemoteToken, p.Insecure, p.Debug)
jenkins, err := NewJenkins(auth, p.BaseURL, p.RemoteToken, p.Insecure, p.CACert, p.Debug)
if err != nil {
return fmt.Errorf("failed to initialize Jenkins client: %w", err)
}
// Parse job parameters
params := parseParameters(p.Parameters)