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 }