mirror of
https://github.com/appleboy/drone-jenkins.git
synced 2026-06-04 10:15:02 +08:00
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>
This commit is contained in:
@@ -41,7 +41,7 @@ A [Drone](https://github.com/drone/drone) plugin for triggering [Jenkins](https:
|
||||
- Multiple authentication methods (API token or remote trigger token)
|
||||
- Wait for job completion with configurable polling and timeout
|
||||
- Debug mode with detailed parameter information and secure token masking
|
||||
- SSL/TLS support with optional insecure mode
|
||||
- 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
|
||||
|
||||
@@ -127,6 +127,7 @@ 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) |
|
||||
@@ -230,6 +231,26 @@ drone-jenkins \
|
||||
--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:**
|
||||
@@ -292,6 +313,29 @@ docker run --rm \
|
||||
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`:
|
||||
@@ -377,6 +421,22 @@ steps:
|
||||
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
|
||||
|
||||
+91
-8
@@ -3,12 +3,14 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -56,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) {
|
||||
|
||||
+236
-10
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user