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