github.com/devtron-labs/ci-runner@v0.0.0-20240518055909-b2672f3349d7/executor/stage/ciStages.go (about) 1 package stage 2 3 import ( 4 "encoding/json" 5 "errors" 6 "github.com/devtron-labs/ci-runner/executor" 7 util2 "github.com/devtron-labs/ci-runner/executor/util" 8 "github.com/devtron-labs/ci-runner/helper" 9 "github.com/devtron-labs/ci-runner/util" 10 "io/ioutil" 11 "log" 12 "os" 13 "time" 14 ) 15 16 /* 17 * Copyright 2020 Devtron Labs 18 * 19 * Licensed under the Apache License, Version 2.0 (the "License"); 20 * you may not use this file except in compliance with the License. 21 * You may obtain a copy of the License at 22 * 23 * http://www.apache.org/licenses/LICENSE-2.0 24 * 25 * Unless required by applicable law or agreed to in writing, software 26 * distributed under the License is distributed on an "AS IS" BASIS, 27 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 * See the License for the specific language governing permissions and 29 * limitations under the License. 30 * 31 */ 32 33 type CiStage struct { 34 gitManager helper.GitManager 35 dockerHelper helper.DockerHelper 36 stageExecutorManager executor.StageExecutor 37 } 38 39 func NewCiStage(gitManager helper.GitManager, dockerHelper helper.DockerHelper, stageExecutor executor.StageExecutor) *CiStage { 40 return &CiStage{ 41 gitManager: gitManager, 42 dockerHelper: dockerHelper, 43 stageExecutorManager: stageExecutor, 44 } 45 } 46 47 func (impl *CiStage) HandleCIEvent(ciCdRequest *helper.CiCdTriggerEvent, exitCode *int) { 48 ciRequest := ciCdRequest.CommonWorkflowRequest 49 artifactUploaded, err := impl.runCIStages(ciCdRequest) 50 log.Println(util.DEVTRON, artifactUploaded, err) 51 var artifactUploadErr error 52 if !artifactUploaded { 53 cloudHelperBaseConfig := ciRequest.GetCloudHelperBaseConfig(util.BlobStorageObjectTypeArtifact) 54 artifactUploaded, artifactUploadErr = helper.ZipAndUpload(cloudHelperBaseConfig, ciCdRequest.CommonWorkflowRequest.CiArtifactFileName) 55 } 56 57 if err != nil { 58 var stageError *helper.CiStageError 59 log.Println(util.DEVTRON, err) 60 if errors.As(err, &stageError) { 61 *exitCode = util.CiStageFailErrorCode 62 return 63 } 64 *exitCode = util.DefaultErrorCode 65 return 66 } 67 68 if artifactUploadErr != nil { 69 log.Println(util.DEVTRON, artifactUploadErr) 70 if ciCdRequest.CommonWorkflowRequest.IsExtRun { 71 log.Println(util.DEVTRON, "Ignoring artifactUploadErr") 72 return 73 } 74 *exitCode = util.DefaultErrorCode 75 return 76 } 77 78 // sync cache 79 log.Println(util.DEVTRON, " cache-push") 80 err = helper.SyncCache(ciRequest) 81 if err != nil { 82 log.Println(err) 83 if ciCdRequest.CommonWorkflowRequest.IsExtRun { 84 log.Println(util.DEVTRON, "Ignoring cache upload") 85 return 86 } 87 *exitCode = util.DefaultErrorCode 88 return 89 } 90 log.Println(util.DEVTRON, " /cache-push") 91 } 92 93 type CiFailReason string 94 95 const ( 96 PreCi CiFailReason = "Pre-CI task failed: " 97 PostCi CiFailReason = "Post-CI task failed: " 98 Build CiFailReason = "Docker build failed" 99 Push CiFailReason = "Docker push failed" 100 Scan CiFailReason = "Image scan failed" 101 ) 102 103 func (impl *CiStage) runCIStages(ciCdRequest *helper.CiCdTriggerEvent) (artifactUploaded bool, err error) { 104 105 metrics := &helper.CIMetrics{} 106 start := time.Now() 107 metrics.TotalStartTime = start 108 artifactUploaded = false 109 110 // change the current working directory to '/' 111 err = os.Chdir(util.HOMEDIR) 112 if err != nil { 113 return artifactUploaded, err 114 } 115 116 // using stat to get check if WORKINGDIR exist or not 117 if _, err := os.Stat(util.WORKINGDIR); os.IsNotExist(err) { 118 // Creating the WORKINGDIR if in case in doesn't exit 119 _ = os.Mkdir(util.WORKINGDIR, os.ModeDir) 120 } 121 122 // Get ci cache 123 log.Println(util.DEVTRON, " cache-pull") 124 start = time.Now() 125 metrics.CacheDownStartTime = start 126 err = helper.GetCache(ciCdRequest.CommonWorkflowRequest) 127 metrics.CacheDownDuration = time.Since(start).Seconds() 128 if err != nil { 129 return artifactUploaded, err 130 } 131 log.Println(util.DEVTRON, " /cache-pull") 132 133 // change the current working directory to WORKINGDIR 134 err = os.Chdir(util.WORKINGDIR) 135 if err != nil { 136 return artifactUploaded, err 137 } 138 // git handling 139 log.Println(util.DEVTRON, " git") 140 ciBuildConfigBean := ciCdRequest.CommonWorkflowRequest.CiBuildConfig 141 buildSkipEnabled := ciBuildConfigBean != nil && ciBuildConfigBean.CiBuildType == helper.BUILD_SKIP_BUILD_TYPE 142 skipCheckout := ciBuildConfigBean != nil && ciBuildConfigBean.PipelineType == helper.CI_JOB 143 if !skipCheckout { 144 err = impl.gitManager.CloneAndCheckout(ciCdRequest.CommonWorkflowRequest.CiProjectDetails) 145 } 146 if err != nil { 147 log.Println(util.DEVTRON, "clone err", err) 148 return artifactUploaded, err 149 } 150 log.Println(util.DEVTRON, " /git") 151 152 // Start docker daemon 153 log.Println(util.DEVTRON, " docker-build") 154 impl.dockerHelper.StartDockerDaemon(ciCdRequest.CommonWorkflowRequest) 155 scriptEnvs, err := util2.GetGlobalEnvVariables(ciCdRequest) 156 if err != nil { 157 return artifactUploaded, err 158 } 159 // Get devtron-ci yaml 160 yamlLocation := ciCdRequest.CommonWorkflowRequest.CheckoutPath 161 log.Println(util.DEVTRON, "devtron-ci yaml location ", yamlLocation) 162 taskYaml, err := helper.GetTaskYaml(yamlLocation) 163 if err != nil { 164 return artifactUploaded, err 165 } 166 ciCdRequest.CommonWorkflowRequest.TaskYaml = taskYaml 167 if ciBuildConfigBean != nil && ciBuildConfigBean.CiBuildType == helper.MANAGED_DOCKERFILE_BUILD_TYPE { 168 err = makeDockerfile(ciBuildConfigBean.DockerBuildConfig, ciCdRequest.CommonWorkflowRequest.CheckoutPath) 169 if err != nil { 170 return artifactUploaded, err 171 } 172 } 173 174 refStageMap := make(map[int][]*helper.StepObject) 175 for _, ref := range ciCdRequest.CommonWorkflowRequest.RefPlugins { 176 refStageMap[ref.Id] = ref.Steps 177 } 178 179 var preCiStageOutVariable map[int]map[string]*helper.VariableObject 180 start = time.Now() 181 metrics.PreCiStartTime = start 182 var resultsFromPlugin *helper.ImageDetailsFromCR 183 if len(ciCdRequest.CommonWorkflowRequest.PreCiSteps) > 0 { 184 resultsFromPlugin, preCiStageOutVariable, err = impl.runPreCiSteps(ciCdRequest, metrics, buildSkipEnabled, refStageMap, scriptEnvs, artifactUploaded) 185 if err != nil { 186 return artifactUploaded, err 187 } 188 } 189 var dest string 190 var digest string 191 if !buildSkipEnabled { 192 dest, digest, err = impl.getImageDestAndDigest(ciCdRequest, metrics, scriptEnvs, refStageMap, preCiStageOutVariable, artifactUploaded) 193 if err != nil { 194 return artifactUploaded, err 195 } 196 } 197 var postCiDuration float64 198 start = time.Now() 199 metrics.PostCiStartTime = start 200 if len(ciCdRequest.CommonWorkflowRequest.PostCiSteps) > 0 { 201 err = impl.runPostCiSteps(ciCdRequest, scriptEnvs, refStageMap, preCiStageOutVariable, metrics, artifactUploaded, dest, digest) 202 postCiDuration = time.Since(start).Seconds() 203 if err != nil { 204 return artifactUploaded, err 205 } 206 } 207 metrics.PostCiDuration = postCiDuration 208 log.Println(util.DEVTRON, " /docker-push") 209 210 log.Println(util.DEVTRON, " artifact-upload") 211 cloudHelperBaseConfig := ciCdRequest.CommonWorkflowRequest.GetCloudHelperBaseConfig(util.BlobStorageObjectTypeArtifact) 212 artifactUploaded, err = helper.ZipAndUpload(cloudHelperBaseConfig, ciCdRequest.CommonWorkflowRequest.CiArtifactFileName) 213 214 if err != nil { 215 return artifactUploaded, nil 216 } 217 //else { 218 // artifactUploaded = true 219 //} 220 log.Println(util.DEVTRON, " /artifact-upload") 221 222 dest, err = impl.dockerHelper.GetDestForNatsEvent(ciCdRequest.CommonWorkflowRequest, dest) 223 if err != nil { 224 return artifactUploaded, err 225 } 226 // scan only if ci scan enabled 227 if helper.IsEventTypeEligibleToScanImage(ciCdRequest.Type) && 228 ciCdRequest.CommonWorkflowRequest.ScanEnabled { 229 err = runImageScanning(dest, digest, ciCdRequest, metrics, artifactUploaded) 230 if err != nil { 231 return artifactUploaded, err 232 } 233 } 234 235 log.Println(util.DEVTRON, " event") 236 metrics.TotalDuration = time.Since(metrics.TotalStartTime).Seconds() 237 238 err = helper.SendEvents(ciCdRequest.CommonWorkflowRequest, digest, dest, *metrics, artifactUploaded, "", resultsFromPlugin) 239 if err != nil { 240 log.Println(err) 241 return artifactUploaded, err 242 } 243 log.Println(util.DEVTRON, " /event") 244 245 err = impl.dockerHelper.StopDocker() 246 if err != nil { 247 log.Println("err", err) 248 return artifactUploaded, err 249 } 250 return artifactUploaded, nil 251 } 252 253 func (impl *CiStage) runPreCiSteps(ciCdRequest *helper.CiCdTriggerEvent, metrics *helper.CIMetrics, 254 buildSkipEnabled bool, refStageMap map[int][]*helper.StepObject, 255 scriptEnvs map[string]string, artifactUploaded bool) (*helper.ImageDetailsFromCR, map[int]map[string]*helper.VariableObject, error) { 256 start := time.Now() 257 metrics.PreCiStartTime = start 258 var resultsFromPlugin *helper.ImageDetailsFromCR 259 if !buildSkipEnabled { 260 util.LogStage("running PRE-CI steps") 261 } 262 // run pre artifact processing 263 preCiStageOutVariable, step, err := impl.stageExecutorManager.RunCiCdSteps(helper.STEP_TYPE_PRE, ciCdRequest.CommonWorkflowRequest, ciCdRequest.CommonWorkflowRequest.PreCiSteps, refStageMap, scriptEnvs, nil) 264 preCiDuration := time.Since(start).Seconds() 265 if err != nil { 266 log.Println("error in running pre Ci Steps", "err", err) 267 err = sendFailureNotification(string(PreCi)+step.Name, ciCdRequest.CommonWorkflowRequest, "", "", *metrics, artifactUploaded, err) 268 return nil, nil, err 269 } 270 // considering pull images from Container repo Plugin in Pre ci steps only. 271 // making it non-blocking if results are not available (in case of err) 272 resultsFromPlugin, err = extractOutResultsIfExists() 273 if err != nil { 274 log.Println("error in getting results", "err", err.Error()) 275 } 276 metrics.PreCiDuration = preCiDuration 277 return resultsFromPlugin, preCiStageOutVariable, nil 278 } 279 280 func (impl *CiStage) runBuildArtifact(ciCdRequest *helper.CiCdTriggerEvent, metrics *helper.CIMetrics, 281 refStageMap map[int][]*helper.StepObject, scriptEnvs map[string]string, artifactUploaded bool, 282 preCiStageOutVariable map[int]map[string]*helper.VariableObject) (string, error) { 283 util.LogStage("Build") 284 // build 285 start := time.Now() 286 metrics.BuildStartTime = start 287 dest, err := impl.dockerHelper.BuildArtifact(ciCdRequest.CommonWorkflowRequest) //TODO make it skipable 288 metrics.BuildDuration = time.Since(start).Seconds() 289 if err != nil { 290 log.Println("Error in building artifact", "err", err) 291 // code-block starts : run post-ci which are enabled to run on ci fail 292 postCiStepsToTriggerOnCiFail := getPostCiStepToRunOnCiFail(ciCdRequest.CommonWorkflowRequest.PostCiSteps) 293 if len(postCiStepsToTriggerOnCiFail) > 0 { 294 util.LogStage("Running POST-CI steps which are enabled to RUN even on CI FAIL") 295 // build success will always be false 296 scriptEnvs[util.ENV_VARIABLE_BUILD_SUCCESS] = "false" 297 // run post artifact processing 298 impl.stageExecutorManager.RunCiCdSteps(helper.STEP_TYPE_POST, ciCdRequest.CommonWorkflowRequest, postCiStepsToTriggerOnCiFail, refStageMap, scriptEnvs, preCiStageOutVariable) 299 } 300 // code-block ends 301 err = sendFailureNotification(string(Build), ciCdRequest.CommonWorkflowRequest, "", "", *metrics, artifactUploaded, err) 302 } 303 log.Println(util.DEVTRON, " Build artifact completed", "dest", dest, "err", err) 304 return dest, err 305 } 306 307 func (impl *CiStage) extractDigest(ciCdRequest *helper.CiCdTriggerEvent, dest string, metrics *helper.CIMetrics, artifactUploaded bool) (string, error) { 308 ciBuildConfigBean := ciCdRequest.CommonWorkflowRequest.CiBuildConfig 309 isBuildX := ciBuildConfigBean != nil && ciBuildConfigBean.DockerBuildConfig != nil && ciBuildConfigBean.DockerBuildConfig.CheckForBuildX() 310 var digest string 311 var err error 312 if isBuildX { 313 digest, err = impl.dockerHelper.ExtractDigestForBuildx(dest) 314 } else { 315 util.LogStage("docker push") 316 // push to dest 317 log.Println(util.DEVTRON, "Docker push Artifact", "dest", dest) 318 impl.pushArtifact(ciCdRequest, dest, digest, metrics, artifactUploaded) 319 digest, err = impl.dockerHelper.ExtractDigestForBuildx(dest) 320 } 321 return digest, err 322 } 323 324 func (impl *CiStage) runPostCiSteps(ciCdRequest *helper.CiCdTriggerEvent, scriptEnvs map[string]string, refStageMap map[int][]*helper.StepObject, preCiStageOutVariable map[int]map[string]*helper.VariableObject, metrics *helper.CIMetrics, artifactUploaded bool, dest string, digest string) error { 325 util.LogStage("running POST-CI steps") 326 // sending build success as true always as post-ci triggers only if ci gets success 327 scriptEnvs[util.ENV_VARIABLE_BUILD_SUCCESS] = "true" 328 scriptEnvs["DEST"] = dest 329 scriptEnvs["DIGEST"] = digest 330 // run post artifact processing 331 _, step, err := impl.stageExecutorManager.RunCiCdSteps(helper.STEP_TYPE_POST, ciCdRequest.CommonWorkflowRequest, ciCdRequest.CommonWorkflowRequest.PostCiSteps, refStageMap, scriptEnvs, preCiStageOutVariable) 332 if err != nil { 333 log.Println("error in running Post Ci Steps", "err", err) 334 return sendFailureNotification(string(PostCi)+step.Name, ciCdRequest.CommonWorkflowRequest, "", "", *metrics, artifactUploaded, err) 335 } 336 return nil 337 } 338 339 func runImageScanning(dest string, digest string, ciCdRequest *helper.CiCdTriggerEvent, metrics *helper.CIMetrics, artifactUploaded bool) error { 340 util.LogStage("IMAGE SCAN") 341 log.Println(util.DEVTRON, " Image Scanning Started for digest", digest) 342 scanEvent := &helper.ScanEvent{ 343 Image: dest, 344 ImageDigest: digest, 345 PipelineId: ciCdRequest.CommonWorkflowRequest.PipelineId, 346 UserId: ciCdRequest.CommonWorkflowRequest.TriggeredBy, 347 DockerRegistryId: ciCdRequest.CommonWorkflowRequest.DockerRegistryId, 348 DockerConnection: ciCdRequest.CommonWorkflowRequest.DockerConnection, 349 DockerCert: ciCdRequest.CommonWorkflowRequest.DockerCert, 350 ImageScanMaxRetries: ciCdRequest.CommonWorkflowRequest.ImageScanMaxRetries, 351 ImageScanRetryDelay: ciCdRequest.CommonWorkflowRequest.ImageScanRetryDelay, 352 } 353 err := helper.SendEventToClairUtility(scanEvent) 354 if err != nil { 355 log.Println("error in running Image Scan", "err", err) 356 err = sendFailureNotification(string(Scan), ciCdRequest.CommonWorkflowRequest, digest, dest, *metrics, artifactUploaded, err) 357 return err 358 } 359 log.Println(util.DEVTRON, "Image scanning completed with scanEvent", scanEvent) 360 return nil 361 } 362 363 func (impl *CiStage) getImageDestAndDigest(ciCdRequest *helper.CiCdTriggerEvent, metrics *helper.CIMetrics, scriptEnvs map[string]string, refStageMap map[int][]*helper.StepObject, preCiStageOutVariable map[int]map[string]*helper.VariableObject, artifactUploaded bool) (string, string, error) { 364 dest, err := impl.runBuildArtifact(ciCdRequest, metrics, refStageMap, scriptEnvs, artifactUploaded, preCiStageOutVariable) 365 if err != nil { 366 return "", "", err 367 } 368 digest, err := impl.extractDigest(ciCdRequest, dest, metrics, artifactUploaded) 369 if err != nil { 370 log.Println("Error in extracting digest", "err", err) 371 return "", "", err 372 } 373 return dest, digest, nil 374 } 375 376 func getPostCiStepToRunOnCiFail(postCiSteps []*helper.StepObject) []*helper.StepObject { 377 var postCiStepsToTriggerOnCiFail []*helper.StepObject 378 if len(postCiSteps) > 0 { 379 for _, postCiStep := range postCiSteps { 380 if postCiStep.TriggerIfParentStageFail { 381 postCiStepsToTriggerOnCiFail = append(postCiStepsToTriggerOnCiFail, postCiStep) 382 } 383 } 384 } 385 return postCiStepsToTriggerOnCiFail 386 } 387 388 // extractOutResultsIfExists will unmarshall the results from file(json) (if file exist) into ImageDetailsFromCR 389 func extractOutResultsIfExists() (*helper.ImageDetailsFromCR, error) { 390 exists, err := util.CheckFileExists(util.ResultsDirInCIRunnerPath) 391 if err != nil || !exists { 392 log.Println("err", err) 393 return nil, err 394 } 395 file, err := ioutil.ReadFile(util.ResultsDirInCIRunnerPath) 396 if err != nil { 397 log.Println("error in reading file", "err", err.Error()) 398 return nil, err 399 } 400 imageDetailsFromCr := helper.ImageDetailsFromCR{} 401 err = json.Unmarshal(file, &imageDetailsFromCr) 402 if err != nil { 403 log.Println("error in unmarshalling imageDetailsFromCr results", "err", err.Error()) 404 return nil, err 405 } 406 return &imageDetailsFromCr, nil 407 408 } 409 410 func makeDockerfile(config *helper.DockerBuildConfig, checkoutPath string) error { 411 dockerfilePath := helper.GetSelfManagedDockerfilePath(checkoutPath) 412 dockerfileContent := config.DockerfileContent 413 f, err := os.Create(dockerfilePath) 414 if err != nil { 415 return err 416 } 417 defer f.Close() 418 _, err = f.WriteString(dockerfileContent) 419 return err 420 } 421 422 func sendFailureNotification(failureMessage string, ciRequest *helper.CommonWorkflowRequest, 423 digest string, image string, ciMetrics helper.CIMetrics, 424 artifactUploaded bool, err error) error { 425 e := helper.SendEvents(ciRequest, digest, image, ciMetrics, artifactUploaded, failureMessage, nil) 426 if e != nil { 427 log.Println(e) 428 return e 429 } 430 return &helper.CiStageError{Err: err} 431 } 432 433 func (impl *CiStage) pushArtifact(ciCdRequest *helper.CiCdTriggerEvent, dest string, digest string, metrics *helper.CIMetrics, artifactUploaded bool) error { 434 imageRetryCountValue := ciCdRequest.CommonWorkflowRequest.ImageRetryCount 435 imageRetryIntervalValue := ciCdRequest.CommonWorkflowRequest.ImageRetryInterval 436 var err error 437 for i := 0; i < imageRetryCountValue+1; i++ { 438 if i != 0 { 439 time.Sleep(time.Duration(imageRetryIntervalValue) * time.Second) 440 } 441 err = impl.dockerHelper.PushArtifact(dest) 442 if err == nil { 443 break 444 } 445 if err != nil { 446 log.Println("Error in pushing artifact", "err", err) 447 } 448 } 449 if err != nil { 450 err = sendFailureNotification(string(Push), ciCdRequest.CommonWorkflowRequest, digest, dest, *metrics, artifactUploaded, err) 451 return err 452 } 453 return err 454 }