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  }