package main import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "path" "strings" "sync" "time" ) // Build represents a Jenkins build type Build struct { Actions []Action `json:"actions"` Number int `json:"number"` URL string `json:"url"` } // Action represents a Jenkins build action type Action struct { Class string `json:"_class"` Parameters []Parameter `json:"parameters,omitempty"` } // Parameter represents a Jenkins build parameter type Parameter struct { Name string `json:"name"` Value interface{} `json:"value"` } // JenkinsClient is a client for interacting with Jenkins API type JenkinsClient struct { Client *http.Client Config *Jenkins semaphore chan struct{} // For concurrency control mu sync.Mutex // For thread safety } // JenkinsService defines the interface for Jenkins operations type JenkinsService interface { FetchBuild(ctx context.Context, job string) (*Build, error) TriggerBuild(ctx context.Context, build *Build, params map[string]interface{}) error FetchBuildWithRetry(ctx context.Context, job string, retries int) (*Build, error) TriggerBuildWithRetry(ctx context.Context, build *Build, params map[string]interface{}, retries int) error } // NewJenkinsClient creates a new Jenkins client func NewJenkinsClient(cfg *Jenkins) *JenkinsClient { // Configure HTTP client with reasonable defaults transport := &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: false, TLSHandshakeTimeout: 5 * time.Second, } return &JenkinsClient{ Client: &http.Client{ Timeout: 15 * time.Second, // Increased from 10s to 15s Transport: transport, }, Config: cfg, semaphore: make(chan struct{}, 5), // Limit to 5 concurrent requests } } // buildAPIURL constructs the Jenkins API URL for a job func (c *JenkinsClient) buildAPIURL(job string) string { return fmt.Sprintf("%s://%s/job/%s/%s/api/json", c.Config.Schema, c.Config.URL, job, c.Config.Number, ) } // FetchBuild retrieves build information from Jenkins func (c *JenkinsClient) FetchBuild(ctx context.Context, job string) (*Build, error) { // Acquire semaphore select { case c.semaphore <- struct{}{}: defer func() { <-c.semaphore }() case <-ctx.Done(): return nil, ctx.Err() } api := c.buildAPIURL(job) log.Printf("[JENKINS] Fetching build info for job: %s from %s", job, api) req, err := http.NewRequestWithContext(ctx, "GET", api, nil) if err != nil { return nil, fmt.Errorf("failed to create GET request for job %s: %w", job, err) } req.SetBasicAuth(c.Config.User, c.Config.Token) req.Header.Set("Accept", "application/json") resp, err := c.Client.Do(req) if err != nil { return nil, fmt.Errorf("request failed for job %s: %w", job, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1000)) return nil, fmt.Errorf("unexpected status %s for job %s: %s", resp.Status, job, string(bodyBytes)) } // Limit response body to 1MB to prevent memory issues body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return nil, fmt.Errorf("failed to read response body for job %s: %w", job, err) } var build Build if err := json.Unmarshal(body, &build); err != nil { return nil, fmt.Errorf("failed to unmarshal response for job %s: %w", job, err) } log.Printf("[JENKINS] Successfully fetched build #%d for job: %s", build.Number, job) return &build, nil } // FetchBuildWithRetry attempts to fetch a build with retries func (c *JenkinsClient) FetchBuildWithRetry(ctx context.Context, job string, retries int) (*Build, error) { var lastErr error for attempt := 0; attempt <= retries; attempt++ { if attempt > 0 { log.Printf("[JENKINS] Retry attempt %d/%d for fetching job: %s", attempt, retries, job) // Exponential backoff: 1s, 2s, 4s, ... backoffDuration := time.Duration(1< 0 { return path.Join(parts[:len(parts)-1]...) + "/buildWithParameters" } return p } // TriggerBuild triggers a Jenkins build with the given parameters func (c *JenkinsClient) TriggerBuild(ctx context.Context, build *Build, params map[string]interface{}) error { // Acquire semaphore select { case c.semaphore <- struct{}{}: defer func() { <-c.semaphore }() case <-ctx.Done(): return ctx.Err() } // Thread safety for params manipulation c.mu.Lock() merged := make(map[string]string) for k, v := range params { merged[k] = fmt.Sprintf("%v", v) } c.mu.Unlock() triggerURL := c.generateBuildURL(build.URL, merged) if triggerURL == "" { return fmt.Errorf("failed to generate trigger URL") } log.Printf("[JENKINS] Triggering build at URL: %s", triggerURL) req, err := http.NewRequestWithContext(ctx, "POST", triggerURL, nil) if err != nil { return fmt.Errorf("failed to create POST request: %w", err) } req.SetBasicAuth(c.Config.User, c.Config.Token) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := c.Client.Do(req) if err != nil { return fmt.Errorf("trigger request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1000)) return fmt.Errorf("trigger failed with status %s: %s", resp.Status, string(bodyBytes)) } log.Printf("[JENKINS] Successfully triggered build: %s", resp.Status) return nil } // TriggerBuildWithRetry attempts to trigger a build with retries func (c *JenkinsClient) TriggerBuildWithRetry(ctx context.Context, build *Build, params map[string]interface{}, retries int) error { var lastErr error for attempt := 0; attempt <= retries; attempt++ { if attempt > 0 { log.Printf("[JENKINS] Retry attempt %d/%d for triggering build", attempt, retries) // Exponential backoff: 1s, 2s, 4s, ... backoffDuration := time.Duration(1<