Skip to content

Commit e2bf318

Browse files
m1ron0xFFEgor Balakin
and
Egor Balakin
authored
kanikoExecute: add multiple build (#4461)
* kanikoExecute: add MultipleImages option --------- Co-authored-by: Egor Balakin <[email protected]>
1 parent b474eb2 commit e2bf318

File tree

4 files changed

+380
-110
lines changed

4 files changed

+380
-110
lines changed

cmd/kanikoExecute.go

+205-76
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"fmt"
5+
"github.com/mitchellh/mapstructure"
56
"strings"
67

78
"github.com/SAP/jenkins-library/pkg/buildsettings"
@@ -134,117 +135,222 @@ func runKanikoExecute(config *kanikoExecuteOptions, telemetryData *telemetry.Cus
134135
}
135136
commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo
136137

137-
if !piperutils.ContainsString(config.BuildOptions, "--destination") {
138-
dest := []string{"--no-push"}
139-
if len(config.ContainerRegistryURL) > 0 && len(config.ContainerImageName) > 0 && len(config.ContainerImageTag) > 0 {
140-
containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
141-
if err != nil {
142-
log.SetErrorCategory(log.ErrorConfiguration)
143-
return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
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)
144176
}
177+
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, image)
178+
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
179+
}
145180

146-
commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
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
147193

148-
// Docker image tags don't allow plus signs in tags, thus replacing with dash
149-
containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
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+
}
150201

151-
if config.ContainerMultiImageBuild {
152-
log.Entry().Debugf("Multi-image build activated for image name '%v'", config.ContainerImageName)
153-
imageListWithFilePath, err := docker.ImageListWithFilePath(config.ContainerImageName, config.ContainerMultiImageBuildExcludes, config.ContainerMultiImageBuildTrimDir, fileUtils)
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)
154208
if err != nil {
155-
return fmt.Errorf("failed to identify image list for multi image build: %w", err)
156-
}
157-
if len(imageListWithFilePath) == 0 {
158-
return fmt.Errorf("no docker files to process, please check exclude list")
209+
log.SetErrorCategory(log.ErrorConfiguration)
210+
return errors.Wrapf(err, "multipleImages: failed to read registry url %v", config.ContainerRegistryURL)
159211
}
160-
for image, file := range imageListWithFilePath {
161-
log.Entry().Debugf("Building image '%v' using file '%v'", image, file)
162-
containerImageNameAndTag := fmt.Sprintf("%v:%v", image, containerImageTag)
163-
dest = []string{"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag)}
164-
buildOpts := append(config.BuildOptions, dest...)
165-
err = runKaniko(file, buildOpts, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment)
166-
if err != nil {
167-
return fmt.Errorf("failed to build image '%v' using '%v': %w", image, file, err)
212+
213+
if entry.ContainerImageTag == "" {
214+
if config.ContainerImageTag == "" {
215+
return fmt.Errorf("both multipleImages containerImageTag and config.containerImageTag are empty")
168216
}
169-
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, image)
170-
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
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+
if err = runKaniko(config.DockerfilePath, buildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
230+
return fmt.Errorf("multipleImages: failed to build image '%v' using '%v': %w", entry.ContainerImageName, config.DockerfilePath, err)
171231
}
172232

173-
// for compatibility reasons also fill single imageNameTag field with "root" image in commonPipelineEnvironment
174-
// only consider if it has been built
175-
// ToDo: reconsider and possibly remove at a later point
176-
if len(imageListWithFilePath[config.ContainerImageName]) > 0 {
177-
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
178-
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
233+
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
234+
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, entry.ContainerImageName)
235+
236+
case entry.ContainerImage != "":
237+
containerImageName, err := docker.ContainerImageNameFromImage(entry.ContainerImage)
238+
if err != nil {
239+
log.SetErrorCategory(log.ErrorConfiguration)
240+
return errors.Wrapf(err, "invalid name part in image %v", entry.ContainerImage)
179241
}
180-
if config.CreateBOM {
181-
//Syft for multi image, generates bom-docker-(1/2/3).xml
182-
return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
242+
containerImageNameTag, err := docker.ContainerImageNameTagFromImage(entry.ContainerImage)
243+
if err != nil {
244+
log.SetErrorCategory(log.ErrorConfiguration)
245+
return errors.Wrapf(err, "invalid tag part in image %v", entry.ContainerImage)
183246
}
184-
return nil
185-
} else {
186-
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, config.ContainerImageName)
187-
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag))
188-
}
189247

190-
log.Entry().Debugf("Single image build for image name '%v'", config.ContainerImageName)
191-
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
192-
dest = []string{"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag)}
193-
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
194-
} else if len(config.ContainerImage) > 0 {
195-
log.Entry().Debugf("Single image build for image '%v'", config.ContainerImage)
196-
containerRegistry, err := docker.ContainerRegistryFromImage(config.ContainerImage)
197-
if err != nil {
198-
log.SetErrorCategory(log.ErrorConfiguration)
199-
return errors.Wrapf(err, "invalid registry part in image %v", config.ContainerImage)
248+
log.Entry().Debugf("multipleImages: image build '%v'", containerImageName)
249+
250+
buildOptions := append(config.BuildOptions,
251+
"--context-sub-path", entry.ContextSubPath,
252+
"--destination", entry.ContainerImage,
253+
)
254+
if err = runKaniko(config.DockerfilePath, buildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
255+
return fmt.Errorf("multipleImages: failed to build image '%v' using '%v': %w", containerImageName, config.DockerfilePath, err)
256+
}
257+
258+
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
259+
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
260+
default:
261+
return fmt.Errorf("multipleImages: either containerImageName or containerImage must be filled")
200262
}
201-
// errors are already caught with previous call to docker.ContainerRegistryFromImage
202-
containerImageName, _ := docker.ContainerImageNameFromImage(config.ContainerImage)
203-
containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(config.ContainerImage)
204-
dest = []string{"--destination", config.ContainerImage}
205-
commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
206-
commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
207-
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
208-
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
209263
}
210-
config.BuildOptions = append(config.BuildOptions, dest...)
211-
} else {
212-
log.Entry().Infof("Running Kaniko build with destination defined via buildOptions: %v", config.BuildOptions)
213264

214-
destination := ""
265+
// for compatibility reasons also fill single imageNameTag field with "root" image in commonPipelineEnvironment
266+
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, config.ContainerImageTag)
267+
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
268+
commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
269+
270+
if config.CreateBOM {
271+
// Syft for multi image, generates bom-docker-(1/2/3).xml
272+
return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
273+
}
274+
return nil
275+
276+
case piperutils.ContainsString(config.BuildOptions, "--destination"):
277+
log.Entry().Infof("Running Kaniko build with destination defined via buildOptions: %v", config.BuildOptions)
215278

216279
for i, o := range config.BuildOptions {
217280
if o == "--destination" && i+1 < len(config.BuildOptions) {
218-
destination = config.BuildOptions[i+1]
219-
break
281+
destination := config.BuildOptions[i+1]
282+
283+
containerRegistry, err := docker.ContainerRegistryFromImage(destination)
284+
if err != nil {
285+
log.SetErrorCategory(log.ErrorConfiguration)
286+
return errors.Wrapf(err, "invalid registry part in image %v", destination)
287+
}
288+
if commonPipelineEnvironment.container.registryURL == "" {
289+
commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
290+
}
291+
292+
// errors are already caught with previous call to docker.ContainerRegistryFromImage
293+
containerImageName, _ := docker.ContainerImageNameFromImage(destination)
294+
containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(destination)
295+
296+
if commonPipelineEnvironment.container.imageNameTag == "" {
297+
commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
298+
}
299+
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
300+
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
220301
}
221302
}
222303

223-
containerRegistry, err := docker.ContainerRegistryFromImage(destination)
304+
case config.ContainerRegistryURL != "" && config.ContainerImageName != "" && config.ContainerImageTag != "":
305+
log.Entry().Debugf("Single image build for image name '%v'", config.ContainerImageName)
224306

307+
containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
225308
if err != nil {
226309
log.SetErrorCategory(log.ErrorConfiguration)
227-
return errors.Wrapf(err, "invalid registry part in image %v", destination)
310+
return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
228311
}
229312

230-
containerImageName, _ := docker.ContainerImageNameFromImage(destination)
231-
containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(destination)
313+
// Docker image tags don't allow plus signs in tags, thus replacing with dash
314+
containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
315+
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
316+
317+
commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
318+
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
319+
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
320+
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, config.ContainerImageName)
321+
config.BuildOptions = append(config.BuildOptions, "--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag))
322+
323+
case config.ContainerImage != "":
324+
log.Entry().Debugf("Single image build for image '%v'", config.ContainerImage)
325+
326+
containerRegistry, err := docker.ContainerRegistryFromImage(config.ContainerImage)
327+
if err != nil {
328+
log.SetErrorCategory(log.ErrorConfiguration)
329+
return errors.Wrapf(err, "invalid registry part in image %v", config.ContainerImage)
330+
}
331+
332+
// errors are already caught with previous call to docker.ContainerRegistryFromImage
333+
containerImageName, _ := docker.ContainerImageNameFromImage(config.ContainerImage)
334+
containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(config.ContainerImage)
232335

233336
commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
234337
commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
235338
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
236339
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
340+
config.BuildOptions = append(config.BuildOptions, "--destination", config.ContainerImage)
341+
default:
342+
config.BuildOptions = append(config.BuildOptions, "--no-push")
237343
}
238344

239-
// no support for building multiple containers
240-
kanikoErr := runKaniko(config.DockerfilePath, config.BuildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment)
241-
if kanikoErr != nil {
242-
return kanikoErr
345+
if err = runKaniko(config.DockerfilePath, config.BuildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
346+
return err
243347
}
348+
244349
if config.CreateBOM {
245350
// Syft for single image, generates bom-docker-0.xml
246351
return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
247352
}
353+
248354
return nil
249355
}
250356

@@ -254,7 +360,9 @@ func runKaniko(dockerFilepath string, buildOptions []string, readDigest bool, ex
254360
return fmt.Errorf("failed to get current working directory: %w", err)
255361
}
256362

257-
kanikoOpts := []string{"--dockerfile", dockerFilepath, "--context", cwd}
363+
// kaniko build context needs a proper prefix, for local directory it is 'dir://'
364+
// for more details see https://github.com/GoogleContainerTools/kaniko#kaniko-build-contexts
365+
kanikoOpts := []string{"--dockerfile", dockerFilepath, "--context", "dir://" + cwd}
258366
kanikoOpts = append(kanikoOpts, buildOptions...)
259367

260368
tmpDir, err := fileUtils.TempDir("", "*-kanikoExecute")
@@ -280,7 +388,6 @@ func runKaniko(dockerFilepath string, buildOptions []string, readDigest bool, ex
280388

281389
if b, err := fileUtils.FileExists(digestFilePath); err == nil && b {
282390
digest, err := fileUtils.FileRead(digestFilePath)
283-
284391
if err != nil {
285392
return errors.Wrap(err, "error while reading image digest")
286393
}
@@ -289,9 +396,31 @@ func runKaniko(dockerFilepath string, buildOptions []string, readDigest bool, ex
289396

290397
log.Entry().Debugf("image digest: %s", digestStr)
291398

292-
commonPipelineEnvironment.container.imageDigest = string(digestStr)
399+
commonPipelineEnvironment.container.imageDigest = digestStr
293400
commonPipelineEnvironment.container.imageDigests = append(commonPipelineEnvironment.container.imageDigests, digestStr)
294401
}
295402

296403
return nil
297404
}
405+
406+
type multipleImageConf struct {
407+
ContextSubPath string `json:"contextSubPath,omitempty"`
408+
ContainerImageName string `json:"containerImageName,omitempty"`
409+
ContainerImageTag string `json:"containerImageTag,omitempty"`
410+
ContainerImage string `json:"containerImage,omitempty"`
411+
}
412+
413+
func parseMultipleImages(src []map[string]interface{}) ([]multipleImageConf, error) {
414+
var result []multipleImageConf
415+
416+
for _, conf := range src {
417+
var structuredConf multipleImageConf
418+
if err := mapstructure.Decode(conf, &structuredConf); err != nil {
419+
return nil, err
420+
}
421+
422+
result = append(result, structuredConf)
423+
}
424+
425+
return result, nil
426+
}

0 commit comments

Comments
 (0)