github.com/jaylevin/jenkins-library@v1.230.4/cmd/kanikoExecute.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/SAP/jenkins-library/pkg/buildsettings"
     8  	"github.com/SAP/jenkins-library/pkg/certutils"
     9  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/SAP/jenkins-library/pkg/command"
    13  	"github.com/SAP/jenkins-library/pkg/docker"
    14  	"github.com/SAP/jenkins-library/pkg/log"
    15  	"github.com/SAP/jenkins-library/pkg/piperutils"
    16  	"github.com/SAP/jenkins-library/pkg/telemetry"
    17  )
    18  
    19  func kanikoExecute(config kanikoExecuteOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *kanikoExecuteCommonPipelineEnvironment) {
    20  	// for command execution use Command
    21  	c := command.Command{
    22  		ErrorCategoryMapping: map[string][]string{
    23  			log.ErrorConfiguration.String(): {
    24  				"unsupported status code 401",
    25  			},
    26  		},
    27  	}
    28  
    29  	// reroute command output to logging framework
    30  	c.Stdout(log.Writer())
    31  	c.Stderr(log.Writer())
    32  
    33  	client := &piperhttp.Client{}
    34  
    35  	fileUtils := &piperutils.Files{}
    36  
    37  	err := runKanikoExecute(&config, telemetryData, commonPipelineEnvironment, &c, client, fileUtils)
    38  	if err != nil {
    39  		log.Entry().WithError(err).Fatal("Kaniko execution failed")
    40  	}
    41  }
    42  
    43  func runKanikoExecute(config *kanikoExecuteOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *kanikoExecuteCommonPipelineEnvironment, execRunner command.ExecRunner, httpClient piperhttp.Sender, fileUtils piperutils.FileUtils) error {
    44  	binfmtSupported, _ := docker.IsBinfmtMiscSupportedByHost(fileUtils)
    45  
    46  	if !binfmtSupported && len(config.TargetArchitectures) > 0 {
    47  		log.Entry().Warning("Be aware that the host doesn't support binfmt_misc and thus multi archtecture docker builds might not be possible")
    48  	}
    49  
    50  	// backward compatibility for parameter ContainerBuildOptions
    51  	if len(config.ContainerBuildOptions) > 0 {
    52  		config.BuildOptions = strings.Split(config.ContainerBuildOptions, " ")
    53  		log.Entry().Warning("Parameter containerBuildOptions is deprecated, please use buildOptions instead.")
    54  		telemetryData.Custom1Label = "ContainerBuildOptions"
    55  		telemetryData.Custom1 = config.ContainerBuildOptions
    56  	}
    57  
    58  	// prepare kaniko container for running with proper Docker config.json and custom certificates
    59  	// custom certificates will be downloaded and appended to ca-certificates.crt file used in container
    60  	if len(config.ContainerPreparationCommand) > 0 {
    61  		prepCommand := strings.Split(config.ContainerPreparationCommand, " ")
    62  		if err := execRunner.RunExecutable(prepCommand[0], prepCommand[1:]...); err != nil {
    63  			return errors.Wrap(err, "failed to initialize Kaniko container")
    64  		}
    65  	}
    66  
    67  	if len(config.CustomTLSCertificateLinks) > 0 {
    68  		err := certutils.CertificateUpdate(config.CustomTLSCertificateLinks, httpClient, fileUtils, "/kaniko/ssl/certs/ca-certificates.crt")
    69  		if err != nil {
    70  			return errors.Wrap(err, "failed to update certificates")
    71  		}
    72  	} else {
    73  		log.Entry().Info("skipping updation of certificates")
    74  	}
    75  
    76  	dockerConfig := []byte(`{"auths":{}}`)
    77  	if len(config.DockerConfigJSON) > 0 {
    78  		var err error
    79  		dockerConfig, err = fileUtils.FileRead(config.DockerConfigJSON)
    80  		if err != nil {
    81  			return errors.Wrapf(err, "failed to read file '%v'", config.DockerConfigJSON)
    82  		}
    83  	}
    84  
    85  	if err := fileUtils.FileWrite("/kaniko/.docker/config.json", dockerConfig, 0644); err != nil {
    86  		return errors.Wrap(err, "failed to write file '/kaniko/.docker/config.json'")
    87  	}
    88  
    89  	log.Entry().Debugf("preparing build settings information...")
    90  	stepName := "kanikoExecute"
    91  	// ToDo: better testability required. So far retrieval of config is rather non deterministic
    92  	dockerImage, err := GetDockerImageValue(stepName)
    93  	if err != nil {
    94  		return fmt.Errorf("failed to retrieve dockerImage configuration: %w", err)
    95  	}
    96  
    97  	kanikoConfig := buildsettings.BuildOptions{
    98  		DockerImage:       dockerImage,
    99  		BuildSettingsInfo: config.BuildSettingsInfo,
   100  	}
   101  
   102  	log.Entry().Debugf("creating build settings information...")
   103  	buildSettingsInfo, err := buildsettings.CreateBuildSettingsInfo(&kanikoConfig, stepName)
   104  	if err != nil {
   105  		log.Entry().Warnf("failed to create build settings info: %v", err)
   106  	}
   107  	commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo
   108  
   109  	if !piperutils.ContainsString(config.BuildOptions, "--destination") {
   110  		dest := []string{"--no-push"}
   111  		if len(config.ContainerRegistryURL) > 0 && len(config.ContainerImageName) > 0 && len(config.ContainerImageTag) > 0 {
   112  			containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
   113  			if err != nil {
   114  				log.SetErrorCategory(log.ErrorConfiguration)
   115  				return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
   116  			}
   117  
   118  			commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
   119  
   120  			// Docker image tags don't allow plus signs in tags, thus replacing with dash
   121  			containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
   122  
   123  			if config.ContainerMultiImageBuild {
   124  				log.Entry().Debugf("Multi-image build activated for image name '%v'", config.ContainerImageName)
   125  				imageListWithFilePath, err := docker.ImageListWithFilePath(config.ContainerImageName, config.ContainerMultiImageBuildExcludes, config.ContainerMultiImageBuildTrimDir, fileUtils)
   126  				if err != nil {
   127  					return fmt.Errorf("failed to identify image list for multi image build: %w", err)
   128  				}
   129  				if len(imageListWithFilePath) == 0 {
   130  					return fmt.Errorf("no docker files to process, please check exclude list")
   131  				}
   132  				for image, file := range imageListWithFilePath {
   133  					log.Entry().Debugf("Building image '%v' using file '%v'", image, file)
   134  					containerImageNameAndTag := fmt.Sprintf("%v:%v", image, containerImageTag)
   135  					dest = []string{"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag)}
   136  					buildOpts := append(config.BuildOptions, dest...)
   137  					err = runKaniko(file, buildOpts, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment)
   138  					if err != nil {
   139  						return fmt.Errorf("failed to build image '%v' using '%v': %w", image, file, err)
   140  					}
   141  					commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, image)
   142  					commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
   143  				}
   144  
   145  				// for compatibility reasons also fill single imageNameTag field with "root" image in commonPipelineEnvironment
   146  				// only consider if it has been built
   147  				// ToDo: reconsider and possibly remove at a later point
   148  				if len(imageListWithFilePath[config.ContainerImageName]) > 0 {
   149  					containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
   150  					commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
   151  				}
   152  
   153  				return nil
   154  			} else {
   155  				commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, config.ContainerImageName)
   156  				commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag))
   157  			}
   158  
   159  			log.Entry().Debugf("Single image build for image name '%v'", config.ContainerImageName)
   160  			containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
   161  			dest = []string{"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag)}
   162  			commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
   163  		} else if len(config.ContainerImage) > 0 {
   164  			log.Entry().Debugf("Single image build for image '%v'", config.ContainerImage)
   165  			containerRegistry, err := docker.ContainerRegistryFromImage(config.ContainerImage)
   166  			if err != nil {
   167  				log.SetErrorCategory(log.ErrorConfiguration)
   168  				return errors.Wrapf(err, "invalid registry part in image %v", config.ContainerImage)
   169  			}
   170  			// errors are already caught with previous call to docker.ContainerRegistryFromImage
   171  			containerImageName, _ := docker.ContainerImageNameFromImage(config.ContainerImage)
   172  			containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(config.ContainerImage)
   173  			dest = []string{"--destination", config.ContainerImage}
   174  			commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
   175  			commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
   176  			commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
   177  			commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
   178  		}
   179  		config.BuildOptions = append(config.BuildOptions, dest...)
   180  	} else {
   181  		log.Entry().Infof("Running Kaniko build with destination defined via buildOptions: %v", config.BuildOptions)
   182  
   183  		destination := ""
   184  
   185  		for i, o := range config.BuildOptions {
   186  			if o == "--destination" && i+1 < len(config.BuildOptions) {
   187  				destination = config.BuildOptions[i+1]
   188  				break
   189  			}
   190  		}
   191  
   192  		containerRegistry, err := docker.ContainerRegistryFromImage(destination)
   193  
   194  		if err != nil {
   195  			log.SetErrorCategory(log.ErrorConfiguration)
   196  			return errors.Wrapf(err, "invalid registry part in image %v", destination)
   197  		}
   198  
   199  		containerImageName, _ := docker.ContainerImageNameFromImage(destination)
   200  		containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(destination)
   201  
   202  		commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
   203  		commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
   204  		commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
   205  		commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
   206  	}
   207  
   208  	// no support for building multiple containers
   209  	return runKaniko(config.DockerfilePath, config.BuildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment)
   210  }
   211  
   212  func runKaniko(dockerFilepath string, buildOptions []string, readDigest bool, execRunner command.ExecRunner, fileUtils piperutils.FileUtils, commonPipelineEnvironment *kanikoExecuteCommonPipelineEnvironment) error {
   213  	cwd, err := fileUtils.Getwd()
   214  	if err != nil {
   215  		return fmt.Errorf("failed to get current working directory: %w", err)
   216  	}
   217  
   218  	kanikoOpts := []string{"--dockerfile", dockerFilepath, "--context", cwd}
   219  	kanikoOpts = append(kanikoOpts, buildOptions...)
   220  
   221  	tmpDir, err := fileUtils.TempDir("", "*-kanikoExecute")
   222  	if err != nil {
   223  		return fmt.Errorf("failed to create tmp dir for kanikoExecute: %w", err)
   224  	}
   225  
   226  	digestFilePath := fmt.Sprintf("%s/digest.txt", tmpDir)
   227  
   228  	if readDigest {
   229  		kanikoOpts = append(kanikoOpts, "--digest-file", digestFilePath)
   230  	}
   231  
   232  	err = execRunner.RunExecutable("/kaniko/executor", kanikoOpts...)
   233  	if err != nil {
   234  		log.SetErrorCategory(log.ErrorBuild)
   235  		return errors.Wrap(err, "execution of '/kaniko/executor' failed")
   236  	}
   237  
   238  	if b, err := fileUtils.FileExists(digestFilePath); err == nil && b {
   239  		digest, err := fileUtils.FileRead(digestFilePath)
   240  
   241  		if err != nil {
   242  			return errors.Wrap(err, "error while reading image digest")
   243  		}
   244  
   245  		digestStr := string(digest)
   246  
   247  		log.Entry().Debugf("image digest: %s", digestStr)
   248  
   249  		commonPipelineEnvironment.container.imageDigest = string(digestStr)
   250  		commonPipelineEnvironment.container.imageDigests = append(commonPipelineEnvironment.container.imageDigests, digestStr)
   251  	}
   252  
   253  	return nil
   254  }