Skip to content

Commit ade40af

Browse files
refactor: address review comments for Dockerfile scan
- Move DockerfileScanRequest and ScanConfig to dedicated bean file - Remove polling logic from InitiateDockerfileScan (fail-fast) - Handle filepath.Abs error and improve error logging - Add panic recovery to asynchronous scan goroutine - Remove unused IgnoredRules field and cleanup code
1 parent fcae229 commit ade40af

3 files changed

Lines changed: 92 additions & 78 deletions

File tree

ci-runner/executor/stage/ciStages.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,11 @@ func (impl *CiStage) runBuildArtifact(ciCdRequest *helper.CiCdTriggerEvent, metr
359359
"checkoutPath", ciCdRequest.CommonWorkflowRequest.CheckoutPath)
360360
// Trigger scan asynchronously (non-blocking, runs parallel to build)
361361
go func() {
362+
defer func() {
363+
if r := recover(); r != nil {
364+
log.Println(util.DEVTRON, "recovered from panic in Dockerfile scan goroutine", "panic", r)
365+
}
366+
}()
362367
log.Println(util.DEVTRON, "dockerfile scan started",
363368
"appId", ciCdRequest.CommonWorkflowRequest.AppId,
364369
"buildId", ciCdRequest.CommonWorkflowRequest.WorkflowId,
Lines changed: 52 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright (c) 2024. Devtron Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package helper
218

319
import (
@@ -11,36 +27,16 @@ import (
1127
"time"
1228

1329
"github.com/caarlos0/env"
30+
"github.com/devtron-labs/ci-runner/helper/bean"
1431
"github.com/devtron-labs/ci-runner/util"
1532
"github.com/go-resty/resty/v2"
1633
)
1734

18-
// DockerfileScanRequest represents the request to scan a Dockerfile
19-
type DockerfileScanRequest struct {
20-
AppId int `json:"appId"`
21-
BuildId int `json:"buildId"`
22-
PipelineId int `json:"pipelineId"`
23-
DockerfileContent string `json:"dockerfileContent"`
24-
DockerfileScanEnabled bool `json:"dockerfileScanEnabled"`
25-
ForceDockerfileScan bool `json:"forceDockerfileScan"`
26-
IgnoredRules []string `json:"ignoredRules"`
27-
}
28-
29-
// ScanConfig holds configuration for Dockerfile scanning
30-
type ScanConfig struct {
31-
ImageScannerEndpoint string `env:"IMAGE_SCANNER_ENDPOINT" envDefault:"http://image-scanner-service.devtroncd:80"`
32-
FailOnError bool `env:"DOCKERFILE_SCAN_FAIL_ON_ERROR" envDefault:"false"`
33-
MaxRetries int `env:"DOCKERFILE_SCAN_MAX_RETRIES" envDefault:"3"`
34-
RetryWaitTimeSeconds int `env:"DOCKERFILE_SCAN_RETRY_WAIT_SECONDS" envDefault:"5"`
35-
}
36-
3735
// MaxDockerfileSize is the maximum allowed Dockerfile size (1MB)
3836
const MaxDockerfileSize = 1 * 1024 * 1024 // 1MB
3937

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

@@ -50,83 +46,62 @@ func InitiateDockerfileScan(ciRequest *CommonWorkflowRequest) {
5046
return
5147
}
5248

53-
// Wait for git clone to complete (checkout path to exist)
54-
// Use the SAME path resolution as Docker build (getDockerfilePath)
49+
// Resolve Dockerfile path
5550
var dockerfilePath string
5651
if ciRequest.CiBuildConfig.CiBuildType == "managed-dockerfile-build" {
57-
// For managed Dockerfile, use GetSelfManagedDockerfilePath
5852
dockerfilePath = filepath.Join(util.WORKINGDIR, ciRequest.CheckoutPath, "./Dockerfile")
5953
} else {
60-
// For self-managed Dockerfile, use the configured path
6154
dockerfilePath = ciRequest.CiBuildConfig.DockerBuildConfig.DockerfilePath
6255
}
63-
// Convert to absolute path (same as Docker build)
64-
dockerfilePath, _ = filepath.Abs(dockerfilePath)
6556

66-
// Fallback wait (should not be needed - scan triggered at Docker build start)
67-
maxWait := 2 * time.Minute
68-
waitInterval := 10 * time.Second
69-
startTime := time.Now()
70-
71-
log.Println(util.DEVTRON, "dockerfile scan: waiting for git clone to complete", "path", dockerfilePath, "buildId", ciRequest.WorkflowId)
57+
// Convert to absolute path
58+
var err error
59+
dockerfilePath, err = filepath.Abs(dockerfilePath)
60+
if err != nil {
61+
log.Println(util.DEVTRON, "error in resolving absolute path for Dockerfile", "path", dockerfilePath, "err", err)
62+
return
63+
}
7264

73-
for time.Since(startTime) < maxWait {
74-
if _, err := os.Stat(dockerfilePath); err == nil {
75-
log.Println(util.DEVTRON, "dockerfile scan: Dockerfile found, proceeding", "path", dockerfilePath, "elapsed", time.Since(startTime).Round(time.Second))
76-
break // File exists, proceed
77-
}
78-
// Log progress every 30 seconds
79-
if int(time.Since(startTime).Seconds())%30 == 0 {
80-
log.Println(util.DEVTRON, "dockerfile scan: waiting for git clone to complete...", "path", dockerfilePath, "elapsed", time.Since(startTime).Round(time.Second), "maxWait", maxWait)
81-
}
82-
time.Sleep(waitInterval)
65+
// Check if Dockerfile exists (removed polling logic)
66+
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
67+
log.Println(util.DEVTRON, "dockerfile scan: Dockerfile not found at", dockerfilePath)
68+
return
8369
}
8470

85-
// Read Dockerfile from filesystem (single source of truth)
71+
// Read Dockerfile from filesystem
8672
dockerfileContent, err := os.ReadFile(dockerfilePath)
8773
if err != nil {
88-
log.Println(util.DEVTRON, "error in reading Dockerfile for scanning",
89-
"path", dockerfilePath, "err", err)
90-
if err := handleScanError(fmt.Sprintf("Failed to read Dockerfile from %s: %v", dockerfilePath, err), ciRequest.DockerfileScanEnabled); err != nil {
91-
return
92-
}
74+
log.Println(util.DEVTRON, "error in reading Dockerfile for scanning", "path", dockerfilePath, "err", err)
75+
handleScanError(fmt.Sprintf("Failed to read Dockerfile: %v", err), ciRequest.DockerfileScanEnabled)
9376
return
9477
}
9578

96-
// Prepare scan request with Dockerfile content
97-
// CRITICAL FIX: Read DockerfileScanEnabled from ciRequest.DockerfileScanEnabled (CommonWorkflowRequest level)
98-
// NOT from ciRequest.CiBuildConfig.DockerBuildConfig.DockerfileScanEnabled (which may be out of sync)
99-
// ForceDockerfileScan is only available at CommonWorkflowRequest level
100-
scanRequest := &DockerfileScanRequest{
79+
// Prepare scan request
80+
scanRequest := &bean.DockerfileScanRequest{
10181
AppId: ciRequest.AppId,
10282
BuildId: ciRequest.WorkflowId,
10383
PipelineId: ciRequest.PipelineId,
10484
DockerfileContent: string(dockerfileContent),
10585
DockerfileScanEnabled: ciRequest.DockerfileScanEnabled,
10686
ForceDockerfileScan: ciRequest.ForceDockerfileScan,
107-
IgnoredRules: []string{}, // Can be populated from config in future
10887
}
10988

11089
jsonBody, err := json.Marshal(scanRequest)
11190
if err != nil {
11291
log.Println(util.DEVTRON, "error in marshalling Dockerfile scan request", "err", err)
113-
if err := handleScanError(fmt.Sprintf("Failed to marshal scan request: %v", err), ciRequest.DockerfileScanEnabled); err != nil {
114-
return
115-
}
92+
handleScanError(fmt.Sprintf("Failed to marshal scan request: %v", err), ciRequest.DockerfileScanEnabled)
11693
return
11794
}
11895

119-
cfg := &ScanConfig{}
96+
cfg := &bean.ScanConfig{}
12097
err = env.Parse(cfg)
12198
if err != nil {
12299
log.Println(util.DEVTRON, "error in parsing scan config", "err", err)
123-
if err := handleScanError(fmt.Sprintf("Failed to parse scan config: %v", err), ciRequest.DockerfileScanEnabled); err != nil {
124-
return
125-
}
100+
handleScanError(fmt.Sprintf("Failed to parse scan config: %v", err), ciRequest.DockerfileScanEnabled)
126101
return
127102
}
128103

129-
// Create HTTP client with timeout and configurable retries
104+
// Create HTTP client (Using local instantiation but with improved error logging)
130105
client := resty.New()
131106
client.SetTimeout(2 * time.Minute)
132107
client.
@@ -139,28 +114,28 @@ func InitiateDockerfileScan(ciRequest *CommonWorkflowRequest) {
139114
SetBody(jsonBody).
140115
Post(fmt.Sprintf("%s/%s", cfg.ImageScannerEndpoint, "scanner/dockerfile/scan"))
141116

142-
// Record success/failure in circuit breaker
117+
// Record success/failure with actual error logging
143118
if err != nil || (resp != nil && (resp.StatusCode() != http.StatusAccepted && resp.StatusCode() != http.StatusOK)) {
144-
log.Println(util.DEVTRON, "circuit breaker recorded FAILURE", "buildId", ciRequest.WorkflowId)
119+
var status string
120+
if resp != nil {
121+
status = resp.Status()
122+
}
123+
log.Println(util.DEVTRON, "circuit breaker recorded FAILURE for Dockerfile scan", "buildId", ciRequest.WorkflowId, "err", err, "status", status)
145124
} else {
146-
log.Println(util.DEVTRON, "circuit breaker recorded SUCCESS", "buildId", ciRequest.WorkflowId)
125+
log.Println(util.DEVTRON, "circuit breaker recorded SUCCESS for Dockerfile scan", "buildId", ciRequest.WorkflowId)
147126
}
148127

149128
if err != nil {
150129
log.Println(util.DEVTRON, "error in calling image-scanner for Dockerfile scan", "err", err)
151-
if err := handleScanError(fmt.Sprintf("Dockerfile scan failed: %v", err), cfg.FailOnError); err != nil {
152-
return
153-
}
130+
handleScanError(fmt.Sprintf("Dockerfile scan failed: %v", err), cfg.FailOnError)
154131
return
155132
}
156133

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

@@ -169,12 +144,11 @@ func InitiateDockerfileScan(ciRequest *CommonWorkflowRequest) {
169144
}
170145

171146
// handleScanError handles scan errors based on FailOnError configuration
172-
func handleScanError(message string, failOnError bool) error {
147+
func handleScanError(message string, failOnError bool) {
173148
if failOnError {
174149
log.Println(util.DEVTRON, "Dockerfile scan failed (fail-on-error enabled)", "message", message)
175-
return fmt.Errorf("Dockerfile scan failed: %s", message)
150+
// We don't return error here because this is called in a goroutine
151+
} else {
152+
log.Println(util.DEVTRON, "Dockerfile scan failed (fail-on-error disabled)", "message", message)
176153
}
177-
// Log warning but don't fail the build
178-
log.Println(util.DEVTRON, "Dockerfile scan failed (fail-on-error disabled)", "message", message)
179-
return nil
180154
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2024. Devtron Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package bean
18+
19+
// DockerfileScanRequest represents the request to scan a Dockerfile
20+
type DockerfileScanRequest struct {
21+
AppId int `json:"appId"`
22+
BuildId int `json:"buildId"`
23+
PipelineId int `json:"pipelineId"`
24+
DockerfileContent string `json:"dockerfileContent"`
25+
DockerfileScanEnabled bool `json:"dockerfileScanEnabled"`
26+
ForceDockerfileScan bool `json:"forceDockerfileScan"`
27+
}
28+
29+
// ScanConfig holds configuration for Dockerfile scanning
30+
type ScanConfig struct {
31+
ImageScannerEndpoint string `env:"IMAGE_SCANNER_ENDPOINT" envDefault:"http://image-scanner-service.devtroncd:80"`
32+
FailOnError bool `env:"DOCKERFILE_SCAN_FAIL_ON_ERROR" envDefault:"false"`
33+
MaxRetries int `env:"DOCKERFILE_SCAN_MAX_RETRIES" envDefault:"3"`
34+
RetryWaitTimeSeconds int `env:"DOCKERFILE_SCAN_RETRY_WAIT_SECONDS" envDefault:"5"`
35+
}

0 commit comments

Comments
 (0)