github.com/SAP/jenkins-library@v1.362.0/cmd/kanikoExecute.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/mitchellh/mapstructure"
     8  	"github.com/pkg/errors"
     9  
    10  	"github.com/SAP/jenkins-library/pkg/buildsettings"
    11  	"github.com/SAP/jenkins-library/pkg/certutils"
    12  	"github.com/SAP/jenkins-library/pkg/command"
    13  	"github.com/SAP/jenkins-library/pkg/docker"
    14  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    15  	"github.com/SAP/jenkins-library/pkg/log"
    16  	"github.com/SAP/jenkins-library/pkg/piperutils"
    17  	"github.com/SAP/jenkins-library/pkg/syft"
    18  	"github.com/SAP/jenkins-library/pkg/telemetry"
    19  )
    20  
    21  func kanikoExecute(config kanikoExecuteOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *kanikoExecuteCommonPipelineEnvironment) {
    22  	// for command execution use Command
    23  	c := command.Command{
    24  		ErrorCategoryMapping: map[string][]string{
    25  			log.ErrorConfiguration.String(): {
    26  				"unsupported status code 401",
    27  			},
    28  		},
    29  		StepName: "kanikoExecute",
    30  	}
    31  
    32  	// reroute command output to logging framework
    33  	c.Stdout(log.Writer())
    34  	c.Stderr(log.Writer())
    35  
    36  	client := &piperhttp.Client{}
    37  
    38  	fileUtils := &piperutils.Files{}
    39  
    40  	err := runKanikoExecute(&config, telemetryData, commonPipelineEnvironment, &c, client, fileUtils)
    41  	if err != nil {
    42  		log.Entry().WithError(err).Fatal("Kaniko execution failed")
    43  	}
    44  }
    45  
    46  func runKanikoExecute(config *kanikoExecuteOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *kanikoExecuteCommonPipelineEnvironment, execRunner command.ExecRunner, httpClient piperhttp.Sender, fileUtils piperutils.FileUtils) error {
    47  	binfmtSupported, _ := docker.IsBinfmtMiscSupportedByHost(fileUtils)
    48  
    49  	if !binfmtSupported && len(config.TargetArchitectures) > 0 {
    50  		log.Entry().Warning("Be aware that the host doesn't support binfmt_misc and thus multi archtecture docker builds might not be possible")
    51  	}
    52  
    53  	// backward compatibility for parameter ContainerBuildOptions
    54  	if len(config.ContainerBuildOptions) > 0 {
    55  		config.BuildOptions = strings.Split(config.ContainerBuildOptions, " ")
    56  		log.Entry().Warning("Parameter containerBuildOptions is deprecated, please use buildOptions instead.")
    57  		telemetryData.ContainerBuildOptions = config.ContainerBuildOptions
    58  	}
    59  
    60  	// prepare kaniko container for running with proper Docker config.json and custom certificates
    61  	// custom certificates will be downloaded and appended to ca-certificates.crt file used in container
    62  	if len(config.ContainerPreparationCommand) > 0 {
    63  		prepCommand := strings.Split(config.ContainerPreparationCommand, " ")
    64  		if err := execRunner.RunExecutable(prepCommand[0], prepCommand[1:]...); err != nil {
    65  			return errors.Wrap(err, "failed to initialize Kaniko container")
    66  		}
    67  	}
    68  
    69  	if len(config.CustomTLSCertificateLinks) > 0 {
    70  		err := certutils.CertificateUpdate(config.CustomTLSCertificateLinks, httpClient, fileUtils, "/kaniko/ssl/certs/ca-certificates.crt")
    71  		if err != nil {
    72  			return errors.Wrap(err, "failed to update certificates")
    73  		}
    74  	} else {
    75  		log.Entry().Info("skipping updation of certificates")
    76  	}
    77  
    78  	dockerConfig := []byte(`{"auths":{}}`)
    79  
    80  	// respect user provided docker config json file
    81  	if len(config.DockerConfigJSON) > 0 {
    82  		var err error
    83  		dockerConfig, err = fileUtils.FileRead(config.DockerConfigJSON)
    84  		if err != nil {
    85  			return errors.Wrapf(err, "failed to read existing docker config json at '%v'", config.DockerConfigJSON)
    86  		}
    87  	}
    88  
    89  	// if : user provided docker config json and registry credentials present then enahance the user provided docker provided json with the registry credentials
    90  	// else if : no user provided docker config json then create a new docker config json for kaniko
    91  	if len(config.DockerConfigJSON) > 0 && len(config.ContainerRegistryURL) > 0 && len(config.ContainerRegistryPassword) > 0 && len(config.ContainerRegistryUser) > 0 {
    92  		targetConfigJson, err := docker.CreateDockerConfigJSON(config.ContainerRegistryURL, config.ContainerRegistryUser, config.ContainerRegistryPassword, "", config.DockerConfigJSON, fileUtils)
    93  		if err != nil {
    94  			return errors.Wrapf(err, "failed to update existing docker config json file '%v'", config.DockerConfigJSON)
    95  		}
    96  
    97  		dockerConfig, err = fileUtils.FileRead(targetConfigJson)
    98  		if err != nil {
    99  			return errors.Wrapf(err, "failed to read enhanced file '%v'", config.DockerConfigJSON)
   100  		}
   101  	} else if len(config.DockerConfigJSON) == 0 && len(config.ContainerRegistryURL) > 0 && len(config.ContainerRegistryPassword) > 0 && len(config.ContainerRegistryUser) > 0 {
   102  		targetConfigJson, err := docker.CreateDockerConfigJSON(config.ContainerRegistryURL, config.ContainerRegistryUser, config.ContainerRegistryPassword, "", "/kaniko/.docker/config.json", fileUtils)
   103  		if err != nil {
   104  			return errors.Wrap(err, "failed to create new docker config json at /kaniko/.docker/config.json")
   105  		}
   106  
   107  		dockerConfig, err = fileUtils.FileRead(targetConfigJson)
   108  		if err != nil {
   109  			return errors.Wrapf(err, "failed to read new docker config file at /kaniko/.docker/config.json")
   110  		}
   111  	}
   112  
   113  	if err := fileUtils.FileWrite("/kaniko/.docker/config.json", dockerConfig, 0644); err != nil {
   114  		return errors.Wrap(err, "failed to write file '/kaniko/.docker/config.json'")
   115  	}
   116  
   117  	log.Entry().Debugf("preparing build settings information...")
   118  	stepName := "kanikoExecute"
   119  	// ToDo: better testability required. So far retrieval of config is rather non deterministic
   120  	dockerImage, err := GetDockerImageValue(stepName)
   121  	if err != nil {
   122  		return fmt.Errorf("failed to retrieve dockerImage configuration: %w", err)
   123  	}
   124  
   125  	kanikoConfig := buildsettings.BuildOptions{
   126  		DockerImage:       dockerImage,
   127  		BuildSettingsInfo: config.BuildSettingsInfo,
   128  	}
   129  
   130  	log.Entry().Debugf("creating build settings information...")
   131  	buildSettingsInfo, err := buildsettings.CreateBuildSettingsInfo(&kanikoConfig, stepName)
   132  	if err != nil {
   133  		log.Entry().Warnf("failed to create build settings info: %v", err)
   134  	}
   135  	commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo
   136  
   137  	switch {
   138  	case config.ContainerMultiImageBuild:
   139  		log.Entry().Debugf("Multi-image build activated for image name '%v'", config.ContainerImageName)
   140  
   141  		if config.ContainerRegistryURL == "" {
   142  			return fmt.Errorf("empty ContainerRegistryURL")
   143  		}
   144  		if config.ContainerImageName == "" {
   145  			return fmt.Errorf("empty ContainerImageName")
   146  		}
   147  		if config.ContainerImageTag == "" {
   148  			return fmt.Errorf("empty ContainerImageTag")
   149  		}
   150  
   151  		containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
   152  		if err != nil {
   153  			log.SetErrorCategory(log.ErrorConfiguration)
   154  			return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
   155  		}
   156  
   157  		commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
   158  
   159  		// Docker image tags don't allow plus signs in tags, thus replacing with dash
   160  		containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
   161  
   162  		imageListWithFilePath, err := docker.ImageListWithFilePath(config.ContainerImageName, config.ContainerMultiImageBuildExcludes, config.ContainerMultiImageBuildTrimDir, fileUtils)
   163  		if err != nil {
   164  			return fmt.Errorf("failed to identify image list for multi image build: %w", err)
   165  		}
   166  		if len(imageListWithFilePath) == 0 {
   167  			return fmt.Errorf("no docker files to process, please check exclude list")
   168  		}
   169  		for image, file := range imageListWithFilePath {
   170  			log.Entry().Debugf("Building image '%v' using file '%v'", image, file)
   171  			containerImageNameAndTag := fmt.Sprintf("%v:%v", image, containerImageTag)
   172  			buildOpts := append(config.BuildOptions, "--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag))
   173  			if err = runKaniko(file, buildOpts, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
   174  				return fmt.Errorf("failed to build image '%v' using '%v': %w", image, file, err)
   175  			}
   176  			commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, image)
   177  			commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
   178  		}
   179  
   180  		// for compatibility reasons also fill single imageNameTag field with "root" image in commonPipelineEnvironment
   181  		// only consider if it has been built
   182  		// ToDo: reconsider and possibly remove at a later point
   183  		if len(imageListWithFilePath[config.ContainerImageName]) > 0 {
   184  			containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
   185  			commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
   186  		}
   187  		if config.CreateBOM {
   188  			// Syft for multi image, generates bom-docker-(1/2/3).xml
   189  			return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
   190  		}
   191  		return nil
   192  
   193  	case config.MultipleImages != nil:
   194  		log.Entry().Debugf("multipleImages build activated")
   195  		parsedMultipleImages, err := parseMultipleImages(config.MultipleImages)
   196  		if err != nil {
   197  			log.SetErrorCategory(log.ErrorConfiguration)
   198  			return errors.Wrap(err, "failed to parse multipleImages param")
   199  		}
   200  
   201  		for _, entry := range parsedMultipleImages {
   202  			switch {
   203  			case entry.ContextSubPath == "":
   204  				return fmt.Errorf("multipleImages: empty contextSubPath")
   205  			case entry.ContainerImageName != "":
   206  				containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
   207  				if err != nil {
   208  					log.SetErrorCategory(log.ErrorConfiguration)
   209  					return errors.Wrapf(err, "multipleImages: failed to read registry url %v", config.ContainerRegistryURL)
   210  				}
   211  
   212  				if entry.ContainerImageTag == "" {
   213  					if config.ContainerImageTag == "" {
   214  						return fmt.Errorf("both multipleImages containerImageTag and config.containerImageTag are empty")
   215  					}
   216  					entry.ContainerImageTag = config.ContainerImageTag
   217  				}
   218  				// Docker image tags don't allow plus signs in tags, thus replacing with dash
   219  				containerImageTag := strings.ReplaceAll(entry.ContainerImageTag, "+", "-")
   220  				containerImageNameAndTag := fmt.Sprintf("%v:%v", entry.ContainerImageName, containerImageTag)
   221  
   222  				log.Entry().Debugf("multipleImages: image build '%v'", entry.ContainerImageName)
   223  
   224  				buildOptions := append(config.BuildOptions,
   225  					"--context-sub-path", entry.ContextSubPath,
   226  					"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag),
   227  				)
   228  
   229  				dockerfilePath := config.DockerfilePath
   230  				if entry.DockerfilePath != "" {
   231  					dockerfilePath = entry.DockerfilePath
   232  				}
   233  
   234  				if err = runKaniko(dockerfilePath, buildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
   235  					return fmt.Errorf("multipleImages: failed to build image '%v' using '%v': %w", entry.ContainerImageName, config.DockerfilePath, err)
   236  				}
   237  
   238  				commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
   239  				commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, entry.ContainerImageName)
   240  
   241  			case entry.ContainerImage != "":
   242  				containerImageName, err := docker.ContainerImageNameFromImage(entry.ContainerImage)
   243  				if err != nil {
   244  					log.SetErrorCategory(log.ErrorConfiguration)
   245  					return errors.Wrapf(err, "invalid name part in image %v", entry.ContainerImage)
   246  				}
   247  				containerImageNameTag, err := docker.ContainerImageNameTagFromImage(entry.ContainerImage)
   248  				if err != nil {
   249  					log.SetErrorCategory(log.ErrorConfiguration)
   250  					return errors.Wrapf(err, "invalid tag part in image %v", entry.ContainerImage)
   251  				}
   252  
   253  				log.Entry().Debugf("multipleImages: image build '%v'", containerImageName)
   254  
   255  				buildOptions := append(config.BuildOptions,
   256  					"--context-sub-path", entry.ContextSubPath,
   257  					"--destination", entry.ContainerImage,
   258  				)
   259  
   260  				dockerfilePath := config.DockerfilePath
   261  				if entry.DockerfilePath != "" {
   262  					dockerfilePath = entry.DockerfilePath
   263  				}
   264  
   265  				if err = runKaniko(dockerfilePath, buildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
   266  					return fmt.Errorf("multipleImages: failed to build image '%v' using '%v': %w", containerImageName, config.DockerfilePath, err)
   267  				}
   268  
   269  				commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
   270  				commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
   271  			default:
   272  				return fmt.Errorf("multipleImages: either containerImageName or containerImage must be filled")
   273  			}
   274  		}
   275  
   276  		// Docker image tags don't allow plus signs in tags, thus replacing with dash
   277  		containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
   278  
   279  		// for compatibility reasons also fill single imageNameTag field with "root" image in commonPipelineEnvironment
   280  		containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
   281  		commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
   282  		commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
   283  
   284  		if config.CreateBOM {
   285  			// Syft for multi image, generates bom-docker-(1/2/3).xml
   286  			return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
   287  		}
   288  		return nil
   289  
   290  	case piperutils.ContainsString(config.BuildOptions, "--destination"):
   291  		log.Entry().Infof("Running Kaniko build with destination defined via buildOptions: %v", config.BuildOptions)
   292  
   293  		for i, o := range config.BuildOptions {
   294  			if o == "--destination" && i+1 < len(config.BuildOptions) {
   295  				destination := config.BuildOptions[i+1]
   296  
   297  				containerRegistry, err := docker.ContainerRegistryFromImage(destination)
   298  				if err != nil {
   299  					log.SetErrorCategory(log.ErrorConfiguration)
   300  					return errors.Wrapf(err, "invalid registry part in image %v", destination)
   301  				}
   302  				if commonPipelineEnvironment.container.registryURL == "" {
   303  					commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
   304  				}
   305  
   306  				// errors are already caught with previous call to docker.ContainerRegistryFromImage
   307  				containerImageName, _ := docker.ContainerImageNameFromImage(destination)
   308  				containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(destination)
   309  
   310  				if commonPipelineEnvironment.container.imageNameTag == "" {
   311  					commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
   312  				}
   313  				commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
   314  				commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
   315  			}
   316  		}
   317  
   318  	case config.ContainerRegistryURL != "" && config.ContainerImageName != "" && config.ContainerImageTag != "":
   319  		log.Entry().Debugf("Single image build for image name '%v'", config.ContainerImageName)
   320  
   321  		containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
   322  		if err != nil {
   323  			log.SetErrorCategory(log.ErrorConfiguration)
   324  			return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
   325  		}
   326  
   327  		// Docker image tags don't allow plus signs in tags, thus replacing with dash
   328  		containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
   329  		containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
   330  
   331  		commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
   332  		commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
   333  		commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
   334  		commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, config.ContainerImageName)
   335  		config.BuildOptions = append(config.BuildOptions, "--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag))
   336  
   337  	case config.ContainerImage != "":
   338  		log.Entry().Debugf("Single image build for image '%v'", config.ContainerImage)
   339  
   340  		containerRegistry, err := docker.ContainerRegistryFromImage(config.ContainerImage)
   341  		if err != nil {
   342  			log.SetErrorCategory(log.ErrorConfiguration)
   343  			return errors.Wrapf(err, "invalid registry part in image %v", config.ContainerImage)
   344  		}
   345  
   346  		// errors are already caught with previous call to docker.ContainerRegistryFromImage
   347  		containerImageName, _ := docker.ContainerImageNameFromImage(config.ContainerImage)
   348  		containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(config.ContainerImage)
   349  
   350  		commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
   351  		commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
   352  		commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
   353  		commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
   354  		config.BuildOptions = append(config.BuildOptions, "--destination", config.ContainerImage)
   355  	default:
   356  		config.BuildOptions = append(config.BuildOptions, "--no-push")
   357  	}
   358  
   359  	if err = runKaniko(config.DockerfilePath, config.BuildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
   360  		return err
   361  	}
   362  
   363  	if config.CreateBOM {
   364  		// Syft for single image, generates bom-docker-0.xml
   365  		return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
   366  	}
   367  
   368  	return nil
   369  }
   370  
   371  func runKaniko(dockerFilepath string, buildOptions []string, readDigest bool, execRunner command.ExecRunner, fileUtils piperutils.FileUtils, commonPipelineEnvironment *kanikoExecuteCommonPipelineEnvironment) error {
   372  	cwd, err := fileUtils.Getwd()
   373  	if err != nil {
   374  		return fmt.Errorf("failed to get current working directory: %w", err)
   375  	}
   376  
   377  	// kaniko build context needs a proper prefix, for local directory it is 'dir://'
   378  	// for more details see https://github.com/GoogleContainerTools/kaniko#kaniko-build-contexts
   379  	kanikoOpts := []string{"--dockerfile", dockerFilepath, "--context", "dir://" + cwd}
   380  	kanikoOpts = append(kanikoOpts, buildOptions...)
   381  
   382  	tmpDir, err := fileUtils.TempDir("", "*-kanikoExecute")
   383  	if err != nil {
   384  		return fmt.Errorf("failed to create tmp dir for kanikoExecute: %w", err)
   385  	}
   386  
   387  	digestFilePath := fmt.Sprintf("%s/digest.txt", tmpDir)
   388  
   389  	if readDigest {
   390  		kanikoOpts = append(kanikoOpts, "--digest-file", digestFilePath)
   391  	}
   392  
   393  	if GeneralConfig.Verbose {
   394  		kanikoOpts = append(kanikoOpts, "--verbosity=debug")
   395  	}
   396  
   397  	err = execRunner.RunExecutable("/kaniko/executor", kanikoOpts...)
   398  	if err != nil {
   399  		log.SetErrorCategory(log.ErrorBuild)
   400  		return errors.Wrap(err, "execution of '/kaniko/executor' failed")
   401  	}
   402  
   403  	if b, err := fileUtils.FileExists(digestFilePath); err == nil && b {
   404  		digest, err := fileUtils.FileRead(digestFilePath)
   405  		if err != nil {
   406  			return errors.Wrap(err, "error while reading image digest")
   407  		}
   408  
   409  		digestStr := string(digest)
   410  
   411  		log.Entry().Debugf("image digest: %s", digestStr)
   412  
   413  		commonPipelineEnvironment.container.imageDigest = digestStr
   414  		commonPipelineEnvironment.container.imageDigests = append(commonPipelineEnvironment.container.imageDigests, digestStr)
   415  	}
   416  
   417  	return nil
   418  }
   419  
   420  type multipleImageConf struct {
   421  	ContextSubPath     string `json:"contextSubPath,omitempty"`
   422  	DockerfilePath     string `json:"dockerfilePath,omitempty"`
   423  	ContainerImageName string `json:"containerImageName,omitempty"`
   424  	ContainerImageTag  string `json:"containerImageTag,omitempty"`
   425  	ContainerImage     string `json:"containerImage,omitempty"`
   426  }
   427  
   428  func parseMultipleImages(src []map[string]interface{}) ([]multipleImageConf, error) {
   429  	var result []multipleImageConf
   430  
   431  	for _, conf := range src {
   432  		var structuredConf multipleImageConf
   433  		if err := mapstructure.Decode(conf, &structuredConf); err != nil {
   434  			return nil, err
   435  		}
   436  
   437  		result = append(result, structuredConf)
   438  	}
   439  
   440  	return result, nil
   441  }