Files
Bo-Yi Wu 351ac33e2d feat: refactor authentication logic and broaden test coverage (#50)
* feat: refactor authentication logic and broaden test coverage

- Improve authentication checks to only require username and token when both are provided
- Update validation logic to allow either (username and token) or remote-token for authentication
- Enhance test coverage for various authentication scenarios
- Refine error messages to indicate a generic authentication requirement instead of specifying missing username or token

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

* style: streamline authentication error handling in config validation

- Simplify authentication error message in config validation

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

---------

Signed-off-by: appleboy <appleboy.tw@gmail.com>
2025-12-27 11:25:36 +08:00

185 lines
5.2 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"log"
"net/url"
"strings"
"time"
)
type (
// Plugin represents the configuration for the Jenkins plugin.
// It contains all necessary credentials and settings to trigger Jenkins jobs.
Plugin struct {
BaseURL string // Jenkins server base URL
Username string // Jenkins username for authentication
Token string // Jenkins API token for authentication
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)
Timeout time.Duration // Maximum time to wait for job completion (default: 30m)
Debug bool // Enable debug mode to show detailed parameter information
}
)
// trimWhitespaceFromSlice removes empty and whitespace-only strings from a slice.
// It returns a new slice containing only non-empty trimmed strings.
func trimWhitespaceFromSlice(items []string) []string {
result := make([]string, 0, len(items))
for _, item := range items {
trimmed := strings.TrimSpace(item)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
// parseParameters converts a multi-line string of key=value pairs into url.Values.
// Each line should contain one key=value pair.
// It logs a warning for any parameters that don't match the expected format.
func parseParameters(params string) url.Values {
values := url.Values{}
// Split by newlines and process each line
lines := strings.Split(params, "\n")
for _, line := range lines {
// Skip empty lines
trimmedLine := strings.TrimSpace(line)
if trimmedLine == "" {
continue
}
parts := strings.SplitN(trimmedLine, "=", 2)
if len(parts) != 2 {
log.Printf(
"warning: skipping invalid parameter format (expected key=value): %q",
trimmedLine,
)
continue
}
key := strings.TrimSpace(parts[0])
value := parts[1] // Keep value as-is to preserve intentional spaces
if key == "" {
log.Printf("warning: skipping parameter with empty key: %q", trimmedLine)
continue
}
values.Add(key, value)
}
return values
}
// validateConfig checks that all required plugin configuration is present.
// It returns a descriptive error if any required field is missing.
func (p Plugin) validateConfig() error {
if p.BaseURL == "" {
return errors.New("jenkins base URL is required")
}
// Validate authentication: either (user + token) or remote-token must be provided
hasUserAuth := p.Username != "" && p.Token != ""
hasRemoteToken := p.RemoteToken != ""
if !hasUserAuth && !hasRemoteToken {
return errors.New("authentication required")
}
return nil
}
// Exec executes the plugin by triggering the configured Jenkins jobs.
// It validates the configuration, parses parameters, and triggers each job sequentially.
// Returns an error if validation fails or any job trigger fails.
// The context can be used to cancel operations mid-execution.
func (p Plugin) Exec(ctx context.Context) error {
// Validate required configuration
if err := p.validateConfig(); err != nil {
return fmt.Errorf("configuration error: %w", err)
}
// Clean and validate job list
jobs := trimWhitespaceFromSlice(p.Job)
if len(jobs) == 0 {
return errors.New("at least one Jenkins job name is required")
}
// Set up authentication (only if username and token are provided)
var auth *Auth
if p.Username != "" && p.Token != "" {
auth = &Auth{
Username: p.Username,
Token: p.Token,
}
}
// Initialize Jenkins client
jenkins, err := NewJenkins(ctx, 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)
// Set default values for wait configuration
pollInterval := p.PollInterval
if pollInterval == 0 {
pollInterval = 10 * time.Second
}
timeout := p.Timeout
if timeout == 0 {
timeout = 30 * time.Minute
}
// Trigger each job
for _, jobName := range jobs {
queueID, err := jenkins.trigger(ctx, jobName, params)
if err != nil {
return fmt.Errorf("failed to trigger job %q: %w", jobName, err)
}
log.Printf("successfully triggered job: %s (queue #%d)", jobName, queueID)
// Wait for job completion if requested
if p.Wait {
buildInfo, err := jenkins.waitForCompletion(
ctx,
jobName,
queueID,
pollInterval,
timeout,
)
if err != nil {
return fmt.Errorf("error waiting for job %q: %w", jobName, err)
}
// Check if build was successful
if buildInfo.Result != "SUCCESS" {
return fmt.Errorf(
"job %q (build #%d) failed with status: %s",
jobName,
buildInfo.Number,
buildInfo.Result,
)
}
log.Printf("job %s (build #%d) completed successfully", jobName, buildInfo.Number)
}
}
return nil
}