Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci-runner/env_gen.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"Category":"DEVTRON","Fields":[{"Env":"AZURE_ACCOUNT_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_ACCOUNT_NAME","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_BLOB_CONTAINER_CI_CACHE","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_BLOB_CONTAINER_CI_LOG","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_GATEWAY_CONNECTION_INSECURE","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_GATEWAY_URL","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_GCP_CREDENTIALS_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_PROVIDER","EnvType":"","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ACCESS_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_BUCKET_VERSIONED","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ENDPOINT","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ENDPOINT_INSECURE","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_SECRET_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CONSUMER_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_BUILD_LOGS_BUCKET","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CACHE_BUCKET","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CACHE_BUCKET_REGION","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CD_LOGS_BUCKET_REGION","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_LOG_TIME_LIMIT","EnvType":"int64","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCANNER_ENDPOINT","EnvType":"string","EnvValue":"http://image-scanner-new-demo-devtroncd-service.devtroncd:80","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"LOG_LEVEL","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_ACK_WAIT_IN_SECS","EnvType":"int","EnvValue":"120","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_BUFFER_SIZE","EnvType":"int","EnvValue":"-1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_MAX_AGE","EnvType":"int","EnvValue":"86400","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_PROCESSING_BATCH_SIZE","EnvType":"int","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_REPLICAS","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_SERVER_HOST","EnvType":"string","EnvValue":"nats://devtron-nats.devtroncd:4222","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_EXPORT_PROM_METRICS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_FAILURE_QUERIES","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_QUERY","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_SLOW_QUERY","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_QUERY_DUR_THRESHOLD","EnvType":"int64","EnvValue":"5000","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"SHOW_DOCKER_BUILD_ARGS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"STREAM_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"}]}]
[{"Category":"DEVTRON","Fields":[{"Env":"AZURE_ACCOUNT_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_ACCOUNT_NAME","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_BLOB_CONTAINER_CI_CACHE","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_BLOB_CONTAINER_CI_LOG","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_GATEWAY_CONNECTION_INSECURE","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"AZURE_GATEWAY_URL","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_GCP_CREDENTIALS_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_PROVIDER","EnvType":"","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ACCESS_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_BUCKET_VERSIONED","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ENDPOINT","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_ENDPOINT_INSECURE","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"BLOB_STORAGE_S3_SECRET_KEY","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"CONSUMER_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_BUILD_LOGS_BUCKET","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CACHE_BUCKET","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CACHE_BUCKET_REGION","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_CD_LOGS_BUCKET_REGION","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DEFAULT_LOG_TIME_LIMIT","EnvType":"int64","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DOCKERFILE_SCAN_FAIL_ON_ERROR","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DOCKERFILE_SCAN_MAX_RETRIES","EnvType":"int","EnvValue":"3","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"DOCKERFILE_SCAN_RETRY_WAIT_SECONDS","EnvType":"int","EnvValue":"5","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"IMAGE_SCANNER_ENDPOINT","EnvType":"string","EnvValue":"http://image-scanner-service.devtroncd:80","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"LOG_LEVEL","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_ACK_WAIT_IN_SECS","EnvType":"int","EnvValue":"120","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_BUFFER_SIZE","EnvType":"int","EnvValue":"-1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_MAX_AGE","EnvType":"int","EnvValue":"86400","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_PROCESSING_BATCH_SIZE","EnvType":"int","EnvValue":"1","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_MSG_REPLICAS","EnvType":"int","EnvValue":"0","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"NATS_SERVER_HOST","EnvType":"string","EnvValue":"nats://devtron-nats.devtroncd:4222","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_EXPORT_PROM_METRICS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_FAILURE_QUERIES","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_ALL_QUERY","EnvType":"bool","EnvValue":"false","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_LOG_SLOW_QUERY","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"PG_QUERY_DUR_THRESHOLD","EnvType":"int64","EnvValue":"5000","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"SHOW_DOCKER_BUILD_ARGS","EnvType":"bool","EnvValue":"true","EnvDescription":"","Example":"","Deprecated":"false"},{"Env":"STREAM_CONFIG_JSON","EnvType":"string","EnvValue":"","EnvDescription":"","Example":"","Deprecated":"false"}]}]
5 changes: 4 additions & 1 deletion ci-runner/env_gen.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
| DEFAULT_CACHE_BUCKET_REGION | string | | | | false |
| DEFAULT_CD_LOGS_BUCKET_REGION | string | | | | false |
| DEFAULT_LOG_TIME_LIMIT | int64 |1 | | | false |
| IMAGE_SCANNER_ENDPOINT | string |http://image-scanner-new-demo-devtroncd-service.devtroncd:80 | | | false |
| DOCKERFILE_SCAN_FAIL_ON_ERROR | bool |false | | | false |
| DOCKERFILE_SCAN_MAX_RETRIES | int |3 | | | false |
| DOCKERFILE_SCAN_RETRY_WAIT_SECONDS | int |5 | | | false |
| IMAGE_SCANNER_ENDPOINT | string |http://image-scanner-service.devtroncd:80 | | | false |
| LOG_LEVEL | int |0 | | | false |
| NATS_MSG_ACK_WAIT_IN_SECS | int |120 | | | false |
| NATS_MSG_BUFFER_SIZE | int |-1 | | | false |
Expand Down
22 changes: 22 additions & 0 deletions ci-runner/executor/stage/ciStages.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,28 @@ func (impl *CiStage) runBuildArtifact(ciCdRequest *helper.CiCdTriggerEvent, metr
// build
start := time.Now()
metrics.BuildStartTime = start

// Trigger Dockerfile scan right before build (git clone has definitely completed)
// Check BOTH flags: user's choice OR policy override
if ciCdRequest.CommonWorkflowRequest.DockerfileScanEnabled || ciCdRequest.CommonWorkflowRequest.ForceDockerfileScan {
log.Println(util.DEVTRON, "dockerfile scan triggered at build start (git clone completed)",
"buildId", ciCdRequest.CommonWorkflowRequest.WorkflowId,
"pipelineId", ciCdRequest.CommonWorkflowRequest.PipelineId,
"appId", ciCdRequest.CommonWorkflowRequest.AppId,
"checkoutPath", ciCdRequest.CommonWorkflowRequest.CheckoutPath)
// Trigger scan asynchronously (non-blocking, runs parallel to build)
go func() {
log.Println(util.DEVTRON, "dockerfile scan started",
"appId", ciCdRequest.CommonWorkflowRequest.AppId,
"buildId", ciCdRequest.CommonWorkflowRequest.WorkflowId,
"pipelineId", ciCdRequest.CommonWorkflowRequest.PipelineId,
"checkoutPath", ciCdRequest.CommonWorkflowRequest.CheckoutPath)
helper.InitiateDockerfileScan(ciCdRequest.CommonWorkflowRequest)
log.Println(util.DEVTRON, "dockerfile scan request sent to image-scanner",
"buildId", ciCdRequest.CommonWorkflowRequest.WorkflowId)
}()
}

dest, err := impl.dockerHelper.BuildArtifact(ciCdRequest.CommonWorkflowRequest) // TODO make it skipable
metrics.BuildDuration = time.Since(start).Seconds()
if err != nil {
Expand Down
180 changes: 180 additions & 0 deletions ci-runner/helper/DockerfileScanHelper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package helper

import (
"crypto/tls"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"time"

"github.com/caarlos0/env"
"github.com/devtron-labs/ci-runner/util"
"github.com/go-resty/resty/v2"
)

// DockerfileScanRequest represents the request to scan a Dockerfile
type DockerfileScanRequest struct {
AppId int `json:"appId"`
BuildId int `json:"buildId"`
PipelineId int `json:"pipelineId"`
DockerfileContent string `json:"dockerfileContent"`
DockerfileScanEnabled bool `json:"dockerfileScanEnabled"`
ForceDockerfileScan bool `json:"forceDockerfileScan"`
IgnoredRules []string `json:"ignoredRules"`
}

// ScanConfig holds configuration for Dockerfile scanning
type ScanConfig struct {
ImageScannerEndpoint string `env:"IMAGE_SCANNER_ENDPOINT" envDefault:"http://image-scanner-service.devtroncd:80"`
FailOnError bool `env:"DOCKERFILE_SCAN_FAIL_ON_ERROR" envDefault:"false"`
MaxRetries int `env:"DOCKERFILE_SCAN_MAX_RETRIES" envDefault:"3"`
RetryWaitTimeSeconds int `env:"DOCKERFILE_SCAN_RETRY_WAIT_SECONDS" envDefault:"5"`
}

// MaxDockerfileSize is the maximum allowed Dockerfile size (1MB)
const MaxDockerfileSize = 1 * 1024 * 1024 // 1MB

// InitiateDockerfileScan initiates a Dockerfile scan using hadolint
// It reads the Dockerfile from filesystem and sends content to image-scanner service
// Note: The decision to run the scan is made by the caller (runBuildArtifact)
// which handles FORCE_DOCKERFILE_SCAN flag and pipeline-level DockerfileScanEnabled settings
func InitiateDockerfileScan(ciRequest *CommonWorkflowRequest) {
log.Println(util.DEVTRON, "initiating Dockerfile scan")

// Validate config exists
if ciRequest.CiBuildConfig == nil || ciRequest.CiBuildConfig.DockerBuildConfig == nil {
log.Println(util.DEVTRON, "docker build config not found, skipping Dockerfile scan")
return
}

// Wait for git clone to complete (checkout path to exist)
// Use the SAME path resolution as Docker build (getDockerfilePath)
var dockerfilePath string
if ciRequest.CiBuildConfig.CiBuildType == "managed-dockerfile-build" {
// For managed Dockerfile, use GetSelfManagedDockerfilePath
dockerfilePath = filepath.Join(util.WORKINGDIR, ciRequest.CheckoutPath, "./Dockerfile")
} else {
// For self-managed Dockerfile, use the configured path
dockerfilePath = ciRequest.CiBuildConfig.DockerBuildConfig.DockerfilePath
}
// Convert to absolute path (same as Docker build)
dockerfilePath, _ = filepath.Abs(dockerfilePath)

// Fallback wait (should not be needed - scan triggered at Docker build start)
maxWait := 2 * time.Minute
waitInterval := 10 * time.Second
startTime := time.Now()

log.Println(util.DEVTRON, "dockerfile scan: waiting for git clone to complete", "path", dockerfilePath, "buildId", ciRequest.WorkflowId)

for time.Since(startTime) < maxWait {
if _, err := os.Stat(dockerfilePath); err == nil {
log.Println(util.DEVTRON, "dockerfile scan: Dockerfile found, proceeding", "path", dockerfilePath, "elapsed", time.Since(startTime).Round(time.Second))
break // File exists, proceed
}
// Log progress every 30 seconds
if int(time.Since(startTime).Seconds())%30 == 0 {
log.Println(util.DEVTRON, "dockerfile scan: waiting for git clone to complete...", "path", dockerfilePath, "elapsed", time.Since(startTime).Round(time.Second), "maxWait", maxWait)
}
time.Sleep(waitInterval)
}

// Read Dockerfile from filesystem (single source of truth)
dockerfileContent, err := os.ReadFile(dockerfilePath)
if err != nil {
log.Println(util.DEVTRON, "error in reading Dockerfile for scanning",
"path", dockerfilePath, "err", err)
if err := handleScanError(fmt.Sprintf("Failed to read Dockerfile from %s: %v", dockerfilePath, err), ciRequest.DockerfileScanEnabled); err != nil {
return
}
return
}

// Prepare scan request with Dockerfile content
// CRITICAL FIX: Read DockerfileScanEnabled from ciRequest.DockerfileScanEnabled (CommonWorkflowRequest level)
// NOT from ciRequest.CiBuildConfig.DockerBuildConfig.DockerfileScanEnabled (which may be out of sync)
// ForceDockerfileScan is only available at CommonWorkflowRequest level
scanRequest := &DockerfileScanRequest{
AppId: ciRequest.AppId,
BuildId: ciRequest.WorkflowId,
PipelineId: ciRequest.PipelineId,
DockerfileContent: string(dockerfileContent),
DockerfileScanEnabled: ciRequest.DockerfileScanEnabled,
ForceDockerfileScan: ciRequest.ForceDockerfileScan,
IgnoredRules: []string{}, // Can be populated from config in future
}

jsonBody, err := json.Marshal(scanRequest)
if err != nil {
log.Println(util.DEVTRON, "error in marshalling Dockerfile scan request", "err", err)
if err := handleScanError(fmt.Sprintf("Failed to marshal scan request: %v", err), ciRequest.DockerfileScanEnabled); err != nil {
return
}
return
}

cfg := &ScanConfig{}
err = env.Parse(cfg)
if err != nil {
log.Println(util.DEVTRON, "error in parsing scan config", "err", err)
if err := handleScanError(fmt.Sprintf("Failed to parse scan config: %v", err), ciRequest.DockerfileScanEnabled); err != nil {
return
}
return
}

// Create HTTP client with timeout and configurable retries
client := resty.New()
client.SetTimeout(2 * time.Minute)
client.
SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).
SetRetryCount(cfg.MaxRetries).
SetRetryWaitTime(time.Duration(cfg.RetryWaitTimeSeconds) * time.Second)

resp, err := client.R().
SetHeader("Content-Type", "application/json").
SetBody(jsonBody).
Post(fmt.Sprintf("%s/%s", cfg.ImageScannerEndpoint, "scanner/dockerfile/scan"))

// Record success/failure in circuit breaker
if err != nil || (resp != nil && (resp.StatusCode() != http.StatusAccepted && resp.StatusCode() != http.StatusOK)) {
log.Println(util.DEVTRON, "circuit breaker recorded FAILURE", "buildId", ciRequest.WorkflowId)
} else {
log.Println(util.DEVTRON, "circuit breaker recorded SUCCESS", "buildId", ciRequest.WorkflowId)
}

if err != nil {
log.Println(util.DEVTRON, "error in calling image-scanner for Dockerfile scan", "err", err)
if err := handleScanError(fmt.Sprintf("Dockerfile scan failed: %v", err), cfg.FailOnError); err != nil {
return
}
return
}

// Accept both 202 (Accepted) and 200 (OK - for cached results)
if resp.StatusCode() != http.StatusAccepted && resp.StatusCode() != http.StatusOK {
log.Println(util.DEVTRON, "image-scanner returned non-202/200 status for Dockerfile scan",
"status", resp.StatusCode(), "body", string(resp.Body()))
if err := handleScanError(fmt.Sprintf("Dockerfile scan failed with status: %d", resp.StatusCode()), cfg.FailOnError); err != nil {
return
}
return
}

log.Println(util.DEVTRON, "successfully initiated Dockerfile scan",
"statusCode", resp.StatusCode(), "buildId", ciRequest.WorkflowId)
}

// handleScanError handles scan errors based on FailOnError configuration
func handleScanError(message string, failOnError bool) error {
if failOnError {
log.Println(util.DEVTRON, "Dockerfile scan failed (fail-on-error enabled)", "message", message)
return fmt.Errorf("Dockerfile scan failed: %s", message)
}
// Log warning but don't fail the build
log.Println(util.DEVTRON, "Dockerfile scan failed (fail-on-error disabled)", "message", message)
return nil
}
2 changes: 2 additions & 0 deletions ci-runner/helper/EventHelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ type CommonWorkflowRequest struct {
AwsInspectorConfig string `json:"awsInspectorConfig,omitempty"`
PartSize int64 `json:"partSize,omitempty"`
ConcurrencyMultiplier int `json:"concurrencyMultiplier,omitempty"`
DockerfileScanEnabled bool `json:"dockerfileScanEnabled,omitempty"`
ForceDockerfileScan bool `json:"forceDockerfileScan,omitempty"`
}

func (c *CommonWorkflowRequest) IsPreCdStage() bool {
Expand Down
Loading