You've already forked sap-jenkins-library
							
							
				mirror of
				https://github.com/SAP/jenkins-library.git
				synced 2025-10-30 23:57:50 +02:00 
			
		
		
		
	cnbBuild step MVP (#3021)
* Implement cnbBuild step Co-authored-by: Benjamin Haegenlaeuer <benjamin.haegenlaeuer@sap.com> * Add cnbBuild groovy test Co-authored-by: Benjamin Haegenlaeuer <benjamin.haegenlaeuer@sap.com> * Add basic documentation template Co-authored-by: Philipp Stehle <philipp.stehle@sap.com> * Support specifiying name, tag and registry Co-authored-by: Pavel Busko <pbusko@users.noreply.github.com> Co-authored-by: Johannes Dillmann <j.dillmann@sap.com> Co-authored-by: Philipp Stehle <philipp.stehle@sap.com> Co-authored-by: Pavel Busko <pbusko@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										187
									
								
								cmd/cnbBuild.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								cmd/cnbBuild.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/command" | ||||
| 	"github.com/SAP/jenkins-library/pkg/docker" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/piperutils" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/docker/cli/cli/config/configfile" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
|  | ||||
| type cnbBuildUtils interface { | ||||
| 	command.ExecRunner | ||||
|  | ||||
| 	FileExists(filename string) (bool, error) | ||||
| 	FileRead(path string) ([]byte, error) | ||||
| 	FileWrite(path string, content []byte, perm os.FileMode) error | ||||
| 	MkdirAll(path string, perm os.FileMode) error | ||||
| 	Getwd() (string, error) | ||||
| 	Glob(pattern string) (matches []string, err error) | ||||
| 	Copy(src, dest string) (int64, error) | ||||
| } | ||||
|  | ||||
| type cnbBuildUtilsBundle struct { | ||||
| 	*command.Command | ||||
| 	*piperutils.Files | ||||
| } | ||||
|  | ||||
| func newCnbBuildUtils() cnbBuildUtils { | ||||
| 	utils := cnbBuildUtilsBundle{ | ||||
| 		Command: &command.Command{}, | ||||
| 		Files:   &piperutils.Files{}, | ||||
| 	} | ||||
| 	utils.Stdout(log.Writer()) | ||||
| 	utils.Stderr(log.Writer()) | ||||
| 	return &utils | ||||
| } | ||||
|  | ||||
| func cnbBuild(config cnbBuildOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *cnbBuildCommonPipelineEnvironment) { | ||||
| 	utils := newCnbBuildUtils() | ||||
|  | ||||
| 	err := runCnbBuild(&config, telemetryData, utils, commonPipelineEnvironment) | ||||
| 	if err != nil { | ||||
| 		log.Entry().WithError(err).Fatal("step execution failed") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func isIgnored(find string) bool { | ||||
| 	return strings.HasSuffix(find, "piper") || strings.Contains(find, ".pipeline") | ||||
| } | ||||
|  | ||||
| func isDir(path string) (bool, error) { | ||||
| 	info, err := os.Stat(path) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	return info.IsDir(), nil | ||||
| } | ||||
|  | ||||
| func runCnbBuild(config *cnbBuildOptions, telemetryData *telemetry.CustomData, utils cnbBuildUtils, commonPipelineEnvironment *cnbBuildCommonPipelineEnvironment) error { | ||||
| 	var err error | ||||
|  | ||||
| 	dockerConfig := &configfile.ConfigFile{} | ||||
| 	dockerConfigJSON := []byte(`{"auths":{}}`) | ||||
| 	if len(config.DockerConfigJSON) > 0 { | ||||
| 		dockerConfigJSON, err = utils.FileRead(config.DockerConfigJSON) | ||||
| 		if err != nil { | ||||
| 			log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 			return errors.Wrapf(err, "failed to read DockerConfigJSON file '%v'", config.DockerConfigJSON) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	err = json.Unmarshal(dockerConfigJSON, dockerConfig) | ||||
| 	if err != nil { | ||||
| 		log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 		return errors.Wrapf(err, "failed to parse DockerConfigJSON file '%v'", config.DockerConfigJSON) | ||||
| 	} | ||||
|  | ||||
| 	auth := map[string]string{} | ||||
| 	for registry, value := range dockerConfig.AuthConfigs { | ||||
| 		auth[registry] = fmt.Sprintf("Basic %s", value.Auth) | ||||
| 	} | ||||
|  | ||||
| 	cnbRegistryAuth, err := json.Marshal(auth) | ||||
| 	if err != nil { | ||||
| 		log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 		return errors.Wrap(err, "failed to marshal DockerConfigJSON") | ||||
| 	} | ||||
|  | ||||
| 	target := "/workspace" | ||||
| 	source, err := utils.Getwd() | ||||
| 	if err != nil { | ||||
| 		log.SetErrorCategory(log.ErrorBuild) | ||||
| 		return errors.Wrap(err, "failed to get current working directory") | ||||
| 	} | ||||
|  | ||||
| 	if len(config.Path) > 0 { | ||||
| 		source = config.Path | ||||
| 	} | ||||
|  | ||||
| 	sourceFiles, _ := utils.Glob(path.Join(source, "**")) | ||||
| 	for _, sourceFile := range sourceFiles { | ||||
| 		if !isIgnored(sourceFile) { | ||||
| 			target := path.Join(target, strings.ReplaceAll(sourceFile, source, "")) | ||||
| 			dir, err := isDir(sourceFile) | ||||
| 			if err != nil { | ||||
| 				log.SetErrorCategory(log.ErrorBuild) | ||||
| 				return errors.Wrapf(err, "Checking file info '%s' failed", target) | ||||
| 			} | ||||
|  | ||||
| 			if dir { | ||||
| 				err = utils.MkdirAll(target, os.ModePerm) | ||||
| 				if err != nil { | ||||
| 					log.SetErrorCategory(log.ErrorBuild) | ||||
| 					return errors.Wrapf(err, "Creating directory '%s' failed", target) | ||||
| 				} | ||||
| 			} else { | ||||
| 				log.Entry().Debugf("Copying '%s' to '%s'", sourceFile, target) | ||||
| 				_, err = utils.Copy(sourceFile, target) | ||||
| 				if err != nil { | ||||
| 					log.SetErrorCategory(log.ErrorBuild) | ||||
| 					return errors.Wrapf(err, "Copying '%s' to '%s' failed", sourceFile, target) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 		} else { | ||||
| 			log.Entry().Debugf("Filterd out '%s'", sourceFile) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var containerImage string | ||||
| 	var containerImageTag string | ||||
|  | ||||
| 	if len(config.ContainerRegistryURL) > 0 && len(config.ContainerImageName) > 0 && len(config.ContainerImageTag) > 0 { | ||||
| 		containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL) | ||||
| 		if err != nil { | ||||
| 			log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 			return errors.Wrapf(err, "failed to read registry url %s", config.ContainerRegistryURL) | ||||
| 		} | ||||
| 		containerImage = fmt.Sprintf("%s/%s", containerRegistry, config.ContainerImageName) | ||||
| 		containerImageTag = strings.ReplaceAll(config.ContainerImageTag, "+", "-") | ||||
| 		commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL | ||||
| 		commonPipelineEnvironment.container.imageNameTag = containerImage | ||||
| 	} else if len(config.ContainerImage) > 0 { | ||||
| 		containerRegistry, err := docker.ContainerRegistryFromImage(config.ContainerImage) | ||||
| 		if err != nil { | ||||
| 			log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 			return errors.Wrapf(err, "invalid registry part in image %s", config.ContainerImage) | ||||
| 		} | ||||
| 		containerImage = config.ContainerImage | ||||
| 		// errors are already caught with previous call to docker.ContainerRegistryFromImage | ||||
| 		containerImageTag, _ := docker.ContainerImageNameTagFromImage(config.ContainerImage) | ||||
| 		commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%s", containerRegistry) | ||||
| 		commonPipelineEnvironment.container.imageNameTag = containerImageTag | ||||
| 	} else { | ||||
| 		log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 		return errors.New("either containerImage or containerImageName and containerImageTag must be present") | ||||
| 	} | ||||
|  | ||||
| 	err = utils.RunExecutable("/cnb/lifecycle/detector") | ||||
| 	if err != nil { | ||||
| 		log.SetErrorCategory(log.ErrorBuild) | ||||
| 		return errors.Wrap(err, "execution of '/cnb/lifecycle/detector' failed") | ||||
| 	} | ||||
|  | ||||
| 	err = utils.RunExecutable("/cnb/lifecycle/builder") | ||||
| 	if err != nil { | ||||
| 		log.SetErrorCategory(log.ErrorBuild) | ||||
| 		return errors.Wrap(err, "execution of '/cnb/lifecycle/builder' failed") | ||||
| 	} | ||||
|  | ||||
| 	utils.AppendEnv([]string{fmt.Sprintf("CNB_REGISTRY_AUTH=%s", string(cnbRegistryAuth))}) | ||||
| 	err = utils.RunExecutable("/cnb/lifecycle/exporter", fmt.Sprintf("%s:%s", containerImage, containerImageTag), fmt.Sprintf("%s:latest", containerImage)) | ||||
| 	if err != nil { | ||||
| 		log.SetErrorCategory(log.ErrorBuild) | ||||
| 		return errors.Wrap(err, "execution of '/cnb/lifecycle/exporter' failed") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										260
									
								
								cmd/cnbBuild_generated.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								cmd/cnbBuild_generated.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,260 @@ | ||||
| // Code generated by piper's step-generator. DO NOT EDIT. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/config" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/piperenv" | ||||
| 	"github.com/SAP/jenkins-library/pkg/splunk" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| type cnbBuildOptions struct { | ||||
| 	ContainerImage       string `json:"containerImage,omitempty"` | ||||
| 	ContainerImageName   string `json:"containerImageName,omitempty"` | ||||
| 	ContainerImageTag    string `json:"containerImageTag,omitempty"` | ||||
| 	ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"` | ||||
| 	Path                 string `json:"path,omitempty"` | ||||
| 	DockerConfigJSON     string `json:"dockerConfigJSON,omitempty"` | ||||
| } | ||||
|  | ||||
| type cnbBuildCommonPipelineEnvironment struct { | ||||
| 	container struct { | ||||
| 		registryURL  string | ||||
| 		imageNameTag string | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (p *cnbBuildCommonPipelineEnvironment) persist(path, resourceName string) { | ||||
| 	content := []struct { | ||||
| 		category string | ||||
| 		name     string | ||||
| 		value    interface{} | ||||
| 	}{ | ||||
| 		{category: "container", name: "registryUrl", value: p.container.registryURL}, | ||||
| 		{category: "container", name: "imageNameTag", value: p.container.imageNameTag}, | ||||
| 	} | ||||
|  | ||||
| 	errCount := 0 | ||||
| 	for _, param := range content { | ||||
| 		err := piperenv.SetResourceParameter(path, resourceName, filepath.Join(param.category, param.name), param.value) | ||||
| 		if err != nil { | ||||
| 			log.Entry().WithError(err).Error("Error persisting piper environment.") | ||||
| 			errCount++ | ||||
| 		} | ||||
| 	} | ||||
| 	if errCount > 0 { | ||||
| 		log.Entry().Fatal("failed to persist Piper environment") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CnbBuildCommand Executes a Cloud Native Buildpacks build for creating a Docker container. | ||||
| func CnbBuildCommand() *cobra.Command { | ||||
| 	const STEP_NAME = "cnbBuild" | ||||
|  | ||||
| 	metadata := cnbBuildMetadata() | ||||
| 	var stepConfig cnbBuildOptions | ||||
| 	var startTime time.Time | ||||
| 	var commonPipelineEnvironment cnbBuildCommonPipelineEnvironment | ||||
| 	var logCollector *log.CollectorHook | ||||
|  | ||||
| 	var createCnbBuildCmd = &cobra.Command{ | ||||
| 		Use:   STEP_NAME, | ||||
| 		Short: "Executes a Cloud Native Buildpacks build for creating a Docker container.", | ||||
| 		Long:  `Executes a Cloud Native Buildpacks build for creating a Docker container.`, | ||||
| 		PreRunE: func(cmd *cobra.Command, _ []string) error { | ||||
| 			startTime = time.Now() | ||||
| 			log.SetStepName(STEP_NAME) | ||||
| 			log.SetVerbose(GeneralConfig.Verbose) | ||||
|  | ||||
| 			GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens) | ||||
|  | ||||
| 			path, _ := os.Getwd() | ||||
| 			fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path} | ||||
| 			log.RegisterHook(fatalHook) | ||||
|  | ||||
| 			err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile) | ||||
| 			if err != nil { | ||||
| 				log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 				return err | ||||
| 			} | ||||
| 			log.RegisterSecret(stepConfig.DockerConfigJSON) | ||||
|  | ||||
| 			if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { | ||||
| 				sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) | ||||
| 				log.RegisterHook(&sentryHook) | ||||
| 			} | ||||
|  | ||||
| 			if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { | ||||
| 				logCollector = &log.CollectorHook{CorrelationID: GeneralConfig.CorrelationID} | ||||
| 				log.RegisterHook(logCollector) | ||||
| 			} | ||||
|  | ||||
| 			return nil | ||||
| 		}, | ||||
| 		Run: func(_ *cobra.Command, _ []string) { | ||||
| 			telemetryData := telemetry.CustomData{} | ||||
| 			telemetryData.ErrorCode = "1" | ||||
| 			handler := func() { | ||||
| 				config.RemoveVaultSecretFiles() | ||||
| 				commonPipelineEnvironment.persist(GeneralConfig.EnvRootPath, "commonPipelineEnvironment") | ||||
| 				telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) | ||||
| 				telemetryData.ErrorCategory = log.GetErrorCategory().String() | ||||
| 				telemetry.Send(&telemetryData) | ||||
| 				if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { | ||||
| 					splunk.Send(&telemetryData, logCollector) | ||||
| 				} | ||||
| 			} | ||||
| 			log.DeferExitHandler(handler) | ||||
| 			defer handler() | ||||
| 			telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) | ||||
| 			if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { | ||||
| 				splunk.Initialize(GeneralConfig.CorrelationID, | ||||
| 					GeneralConfig.HookConfig.SplunkConfig.Dsn, | ||||
| 					GeneralConfig.HookConfig.SplunkConfig.Token, | ||||
| 					GeneralConfig.HookConfig.SplunkConfig.Index, | ||||
| 					GeneralConfig.HookConfig.SplunkConfig.SendLogs) | ||||
| 			} | ||||
| 			cnbBuild(stepConfig, &telemetryData, &commonPipelineEnvironment) | ||||
| 			telemetryData.ErrorCode = "0" | ||||
| 			log.Entry().Info("SUCCESS") | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	addCnbBuildFlags(createCnbBuildCmd, &stepConfig) | ||||
| 	return createCnbBuildCmd | ||||
| } | ||||
|  | ||||
| func addCnbBuildFlags(cmd *cobra.Command, stepConfig *cnbBuildOptions) { | ||||
| 	cmd.Flags().StringVar(&stepConfig.ContainerImage, "containerImage", os.Getenv("PIPER_containerImage"), "Defines the full name of the Docker image to be created including registry, image name and tag like `my.docker.registry/path/myImageName:myTag`. If left empty, image will not be pushed.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.ContainerImageName, "containerImageName", os.Getenv("PIPER_containerImageName"), "Name of the container which will be built - will be used instead of parameter `containerImage`") | ||||
| 	cmd.Flags().StringVar(&stepConfig.ContainerImageTag, "containerImageTag", os.Getenv("PIPER_containerImageTag"), "Tag of the container which will be built - will be used instead of parameter `containerImage`") | ||||
| 	cmd.Flags().StringVar(&stepConfig.ContainerRegistryURL, "containerRegistryUrl", os.Getenv("PIPER_containerRegistryUrl"), "http(s) url of the Container registry where the image should be pushed to - will be used instead of parameter `containerImage`") | ||||
| 	cmd.Flags().StringVar(&stepConfig.Path, "path", os.Getenv("PIPER_path"), "The path should either point to your sources or an artifact build before.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.DockerConfigJSON, "dockerConfigJSON", os.Getenv("PIPER_dockerConfigJSON"), "Path to the file `.docker/config.json` - this is typically provided by your CI/CD system. You can find more details about the Docker credentials in the [Docker documentation](https://docs.docker.com/engine/reference/commandline/login/).") | ||||
|  | ||||
| 	cmd.MarkFlagRequired("dockerConfigJSON") | ||||
| } | ||||
|  | ||||
| // retrieve step metadata | ||||
| func cnbBuildMetadata() config.StepData { | ||||
| 	var theMetaData = config.StepData{ | ||||
| 		Metadata: config.StepMetadata{ | ||||
| 			Name:        "cnbBuild", | ||||
| 			Aliases:     []config.Alias{}, | ||||
| 			Description: "Executes a Cloud Native Buildpacks build for creating a Docker container.", | ||||
| 		}, | ||||
| 		Spec: config.StepSpec{ | ||||
| 			Inputs: config.StepInputs{ | ||||
| 				Secrets: []config.StepSecrets{ | ||||
| 					{Name: "dockerConfigJsonCredentialsId", Description: "Jenkins 'Secret file' credentials ID containing Docker config.json (with registry credential(s)). You can create it like explained in the Docker Success Center in the article about [how to generate a new auth in the config.json file](https://success.docker.com/article/generate-new-auth-in-config-json-file).", Type: "jenkins"}, | ||||
| 				}, | ||||
| 				Parameters: []config.StepParameters{ | ||||
| 					{ | ||||
| 						Name:        "containerImage", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 						Default:     os.Getenv("PIPER_containerImage"), | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "containerImageName", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{{Name: "dockerImageName"}}, | ||||
| 						Default:     os.Getenv("PIPER_containerImageName"), | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "containerImageTag", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "commonPipelineEnvironment", | ||||
| 								Param: "artifactVersion", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: false, | ||||
| 						Aliases:   []config.Alias{{Name: "artifactVersion"}}, | ||||
| 						Default:   os.Getenv("PIPER_containerImageTag"), | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "containerRegistryUrl", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "commonPipelineEnvironment", | ||||
| 								Param: "container/registryUrl", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: false, | ||||
| 						Aliases:   []config.Alias{{Name: "dockerRegistryUrl"}}, | ||||
| 						Default:   os.Getenv("PIPER_containerRegistryUrl"), | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "path", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 						Default:     os.Getenv("PIPER_path"), | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "dockerConfigJSON", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "commonPipelineEnvironment", | ||||
| 								Param: "custom/dockerConfigJSON", | ||||
| 							}, | ||||
|  | ||||
| 							{ | ||||
| 								Name: "dockerConfigJsonCredentialsId", | ||||
| 								Type: "secret", | ||||
| 							}, | ||||
|  | ||||
| 							{ | ||||
| 								Name:  "", | ||||
| 								Paths: []string{"$(vaultPath)/docker-config", "$(vaultBasePath)/$(vaultPipelineName)/docker-config", "$(vaultBasePath)/GROUP-SECRETS/docker-config"}, | ||||
| 								Type:  "vaultSecretFile", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: true, | ||||
| 						Aliases:   []config.Alias{}, | ||||
| 						Default:   os.Getenv("PIPER_dockerConfigJSON"), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Containers: []config.Container{ | ||||
| 				{Image: "paketobuildpacks/builder:full"}, | ||||
| 			}, | ||||
| 			Outputs: config.StepOutputs{ | ||||
| 				Resources: []config.StepResources{ | ||||
| 					{ | ||||
| 						Name: "commonPipelineEnvironment", | ||||
| 						Type: "piperEnvironment", | ||||
| 						Parameters: []map[string]interface{}{ | ||||
| 							{"Name": "container/registryUrl"}, | ||||
| 							{"Name": "container/imageNameTag"}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	return theMetaData | ||||
| } | ||||
							
								
								
									
										17
									
								
								cmd/cnbBuild_generated_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cmd/cnbBuild_generated_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestCnbBuildCommand(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	testCmd := CnbBuildCommand() | ||||
|  | ||||
| 	// only high level testing performed - details are tested in step generation procedure | ||||
| 	assert.Equal(t, "cnbBuild", testCmd.Use, "command name incorrect") | ||||
|  | ||||
| } | ||||
							
								
								
									
										79
									
								
								cmd/cnbBuild_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								cmd/cnbBuild_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/mock" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| type cnbBuildMockUtils struct { | ||||
| 	*mock.ExecMockRunner | ||||
| 	*mock.FilesMock | ||||
| } | ||||
|  | ||||
| func newCnbBuildTestsUtils() cnbBuildMockUtils { | ||||
| 	utils := cnbBuildMockUtils{ | ||||
| 		ExecMockRunner: &mock.ExecMockRunner{}, | ||||
| 		FilesMock:      &mock.FilesMock{}, | ||||
| 	} | ||||
| 	return utils | ||||
| } | ||||
|  | ||||
| func TestRunCnbBuild(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	commonPipelineEnvironment := cnbBuildCommonPipelineEnvironment{} | ||||
|  | ||||
| 	t.Run("success case", func(t *testing.T) { | ||||
| 		t.Parallel() | ||||
| 		registy := "some-registry" | ||||
| 		config := cnbBuildOptions{ | ||||
| 			ContainerImageName:   "my-image", | ||||
| 			ContainerImageTag:    "0.0.1", | ||||
| 			ContainerRegistryURL: fmt.Sprintf("https://%s", registy), | ||||
| 			DockerConfigJSON:     "/path/to/config.json", | ||||
| 		} | ||||
|  | ||||
| 		utils := newCnbBuildTestsUtils() | ||||
| 		utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":{"auth":"dXNlcjpwYXNz"}}}`)) | ||||
|  | ||||
| 		err := runCnbBuild(&config, &telemetry.CustomData{}, utils, &commonPipelineEnvironment) | ||||
|  | ||||
| 		assert.NoError(t, err) | ||||
| 		runner := utils.ExecMockRunner | ||||
| 		assert.Contains(t, runner.Env, "CNB_REGISTRY_AUTH={\"my-registry\":\"Basic dXNlcjpwYXNz\"}") | ||||
| 		assert.Equal(t, "/cnb/lifecycle/detector", runner.Calls[0].Exec) | ||||
| 		assert.Equal(t, "/cnb/lifecycle/builder", runner.Calls[1].Exec) | ||||
| 		assert.Equal(t, "/cnb/lifecycle/exporter", runner.Calls[2].Exec) | ||||
| 		assert.Equal(t, []string{fmt.Sprintf("%s/%s:%s", registy, config.ContainerImageName, config.ContainerImageTag), fmt.Sprintf("%s/%s:latest", registy, config.ContainerImageName)}, runner.Calls[2].Params) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("error case: Invalid DockerConfigJSON file", func(t *testing.T) { | ||||
| 		t.Parallel() | ||||
| 		config := cnbBuildOptions{ | ||||
| 			ContainerImage:   "my-image", | ||||
| 			DockerConfigJSON: "/path/to/config.json", | ||||
| 		} | ||||
|  | ||||
| 		utils := newCnbBuildTestsUtils() | ||||
| 		utils.FilesMock.AddFile(config.DockerConfigJSON, []byte(`{"auths":{"my-registry":"dXNlcjpwYXNz"}}`)) | ||||
|  | ||||
| 		err := runCnbBuild(&config, nil, utils, &commonPipelineEnvironment) | ||||
| 		assert.EqualError(t, err, "failed to parse DockerConfigJSON file '/path/to/config.json': json: cannot unmarshal string into Go struct field ConfigFile.auths of type types.AuthConfig") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("error case: DockerConfigJSON file not there", func(t *testing.T) { | ||||
| 		t.Parallel() | ||||
| 		config := cnbBuildOptions{ | ||||
| 			ContainerImage:   "my-image", | ||||
| 			DockerConfigJSON: "not-there", | ||||
| 		} | ||||
|  | ||||
| 		utils := newCnbBuildTestsUtils() | ||||
| 		err := runCnbBuild(&config, nil, utils, &commonPipelineEnvironment) | ||||
| 		assert.EqualError(t, err, "failed to read DockerConfigJSON file 'not-there': could not read 'not-there'") | ||||
| 	}) | ||||
| } | ||||
| @@ -30,6 +30,7 @@ func GetAllStepMetadata() map[string]config.StepData { | ||||
| 		"cloudFoundryDeleteService":                 cloudFoundryDeleteServiceMetadata(), | ||||
| 		"cloudFoundryDeleteSpace":                   cloudFoundryDeleteSpaceMetadata(), | ||||
| 		"cloudFoundryDeploy":                        cloudFoundryDeployMetadata(), | ||||
| 		"cnbBuild":                                  cnbBuildMetadata(), | ||||
| 		"containerExecuteStructureTests":            containerExecuteStructureTestsMetadata(), | ||||
| 		"detectExecuteScan":                         detectExecuteScanMetadata(), | ||||
| 		"fortifyExecuteScan":                        fortifyExecuteScanMetadata(), | ||||
|   | ||||
| @@ -126,6 +126,7 @@ func Execute() { | ||||
| 	rootCmd.AddCommand(GctsCloneRepositoryCommand()) | ||||
| 	rootCmd.AddCommand(JsonApplyPatchCommand()) | ||||
| 	rootCmd.AddCommand(KanikoExecuteCommand()) | ||||
| 	rootCmd.AddCommand(CnbBuildCommand()) | ||||
| 	rootCmd.AddCommand(AbapEnvironmentAssemblePackagesCommand()) | ||||
| 	rootCmd.AddCommand(AbapAddonAssemblyKitCheckCVsCommand()) | ||||
| 	rootCmd.AddCommand(AbapAddonAssemblyKitCheckPVCommand()) | ||||
|   | ||||
							
								
								
									
										9
									
								
								documentation/docs/steps/cnbBuild.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								documentation/docs/steps/cnbBuild.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # ${docGenStepName} | ||||
|  | ||||
| ## ${docGenDescription} | ||||
|  | ||||
| ## ${docGenParameters} | ||||
|  | ||||
| ## ${docGenConfiguration} | ||||
|  | ||||
| ## ${docJenkinsPluginDependencies} | ||||
| @@ -75,6 +75,7 @@ nav: | ||||
|         - cloudFoundryCreateServiceKey: steps/cloudFoundryCreateServiceKey.md | ||||
|         - cloudFoundryDeleteService: steps/cloudFoundryDeleteService.md | ||||
|         - cloudFoundryDeploy: steps/cloudFoundryDeploy.md | ||||
|         - cnbBuild: steps/cnbBuild.md | ||||
|         - commonPipelineEnvironment: steps/commonPipelineEnvironment.md | ||||
|         - containerExecuteStructureTests: steps/containerExecuteStructureTests.md | ||||
|         - containerPushToRegistry: steps/containerPushToRegistry.md | ||||
|   | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @@ -12,6 +12,7 @@ require ( | ||||
| 	github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect | ||||
| 	github.com/bmatcuk/doublestar v1.3.2 | ||||
| 	github.com/bndr/gojenkins v1.1.1-0.20210520222939-90ed82bfdff6 | ||||
| 	github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017 | ||||
| 	github.com/elliotchance/orderedmap v1.3.0 | ||||
| 	github.com/evanphx/json-patch v4.9.0+incompatible | ||||
| 	github.com/getsentry/sentry-go v0.7.0 | ||||
|   | ||||
							
								
								
									
										24
									
								
								integration/integration_cnb_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								integration/integration_cnb_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // +build integration | ||||
| // can be execute with go test -tags=integration ./integration/... | ||||
|  | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestNpmProject(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ | ||||
| 		Image:   "paketobuildpacks/builder:full", | ||||
| 		User:    "cnb", | ||||
| 		TestDir: []string{"testdata", "TestMtaIntegration", "npm"}, | ||||
| 	}) | ||||
|  | ||||
| 	container.whenRunningPiperCommand("cnbBuild", "--containerImage", "not-found") | ||||
|  | ||||
| 	container.assertHasOutput(t, "running command: /cnb/lifecycle/detector") | ||||
| 	container.assertHasOutput(t, "Paketo NPM Start Buildpack") | ||||
| 	container.assertHasOutput(t, "Saving not-found") | ||||
| 	container.assertHasOutput(t, "failed to write image to the following tags: [not-found:") | ||||
| } | ||||
							
								
								
									
										87
									
								
								resources/metadata/cnbBuild.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								resources/metadata/cnbBuild.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| metadata: | ||||
|   name: cnbBuild | ||||
|   description: Executes a Cloud Native Buildpacks build for creating a Docker container. | ||||
|   longDescription: Executes a Cloud Native Buildpacks build for creating a Docker container. | ||||
| spec: | ||||
|   inputs: | ||||
|     secrets: | ||||
|       - name: dockerConfigJsonCredentialsId | ||||
|         description: Jenkins 'Secret file' credentials ID containing Docker config.json (with registry credential(s)). You can create it like explained in the Docker Success Center in the article about [how to generate a new auth in the config.json file](https://success.docker.com/article/generate-new-auth-in-config-json-file). | ||||
|         type: jenkins | ||||
|     params: | ||||
|       - name: containerImage | ||||
|         type: string | ||||
|         description: Defines the full name of the Docker image to be created including registry, image name and tag like `my.docker.registry/path/myImageName:myTag`. If left empty, image will not be pushed. | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|       - name: containerImageName | ||||
|         aliases: | ||||
|           - name: dockerImageName | ||||
|         type: string | ||||
|         description: Name of the container which will be built - will be used instead of parameter `containerImage` | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|       - name: containerImageTag | ||||
|         aliases: | ||||
|           - name: artifactVersion | ||||
|         type: string | ||||
|         description: Tag of the container which will be built - will be used instead of parameter `containerImage` | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         resourceRef: | ||||
|           - name: commonPipelineEnvironment | ||||
|             param: artifactVersion | ||||
|       - name: containerRegistryUrl | ||||
|         aliases: | ||||
|           - name: dockerRegistryUrl | ||||
|         type: string | ||||
|         description: http(s) url of the Container registry where the image should be pushed to - will be used instead of parameter `containerImage` | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         resourceRef: | ||||
|           - name: commonPipelineEnvironment | ||||
|             param: container/registryUrl | ||||
|       - name: path | ||||
|         type: string | ||||
|         description: The path should either point to your sources or an artifact build before. | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|       - name: dockerConfigJSON | ||||
|         type: string | ||||
|         description: Path to the file `.docker/config.json` - this is typically provided by your CI/CD system. You can find more details about the Docker credentials in the [Docker documentation](https://docs.docker.com/engine/reference/commandline/login/). | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|         secret: true | ||||
|         mandatory: true | ||||
|         resourceRef: | ||||
|           - name: commonPipelineEnvironment | ||||
|             param: custom/dockerConfigJSON | ||||
|           - name: dockerConfigJsonCredentialsId | ||||
|             type: secret | ||||
|           - type: vaultSecretFile | ||||
|             paths: | ||||
|               - $(vaultPath)/docker-config | ||||
|               - $(vaultBasePath)/$(vaultPipelineName)/docker-config | ||||
|               - $(vaultBasePath)/GROUP-SECRETS/docker-config | ||||
|   outputs: | ||||
|     resources: | ||||
|       - name: commonPipelineEnvironment | ||||
|         type: piperEnvironment | ||||
|         params: | ||||
|           - name: container/registryUrl | ||||
|           - name: container/imageNameTag | ||||
|   containers: | ||||
|     - image: "paketobuildpacks/builder:full" | ||||
							
								
								
									
										61
									
								
								test/groovy/CnbBuildTest.groovy
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								test/groovy/CnbBuildTest.groovy
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| import org.junit.Rule | ||||
| import org.junit.Test | ||||
| import org.junit.rules.RuleChain | ||||
|  | ||||
| import util.BasePiperTest | ||||
| import util.JenkinsReadYamlRule | ||||
| import util.JenkinsStepRule | ||||
| import util.Rules | ||||
|  | ||||
| import static org.junit.Assert.assertThat | ||||
| import static org.hamcrest.Matchers.is | ||||
| import static org.hamcrest.Matchers.hasEntry | ||||
| import static org.hamcrest.Matchers.allOf | ||||
|  | ||||
| public class CnbBuildTest extends BasePiperTest { | ||||
|  | ||||
|     private JenkinsStepRule stepRule = new JenkinsStepRule(this) | ||||
|     private JenkinsReadYamlRule readYamlRule = new JenkinsReadYamlRule(this) | ||||
|  | ||||
|     @Rule | ||||
|     public RuleChain ruleChain = Rules | ||||
|         .getCommonRules(this) | ||||
|         .around(stepRule) | ||||
|         .around(readYamlRule) | ||||
|  | ||||
|     @Test | ||||
|     void testCallGoWrapper() { | ||||
|  | ||||
|         def calledWithParameters, | ||||
|             calledWithStepName, | ||||
|             calledWithMetadata, | ||||
|             calledWithCredentials | ||||
|  | ||||
|         helper.registerAllowedMethod( | ||||
|             'piperExecuteBin', | ||||
|             [Map, String, String, List], | ||||
|             { | ||||
|                 params, stepName, metaData, creds -> | ||||
|                 calledWithParameters = params | ||||
|                 calledWithStepName = stepName | ||||
|                 calledWithMetadata = metaData | ||||
|                 calledWithCredentials = creds | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         stepRule.step.cnbBuild(script: nullScript, containerImage: 'foo:bar', dockerConfigJsonCredentialsId: 'DOCKER_CREDENTIALS') | ||||
|  | ||||
|         assertThat(calledWithParameters.size(), is(3)) | ||||
|         assertThat(calledWithParameters.script, is(nullScript)) | ||||
|         assertThat(calledWithParameters.containerImage, is('foo:bar')) | ||||
|         assertThat(calledWithParameters.dockerConfigJsonCredentialsId, is('DOCKER_CREDENTIALS')) | ||||
|  | ||||
|         assertThat(calledWithStepName, is('cnbBuild')) | ||||
|         assertThat(calledWithMetadata, is('metadata/cnbBuild.yaml')) | ||||
|  | ||||
|         assertThat(calledWithCredentials.size(), is(1)) | ||||
|         assertThat(calledWithCredentials[0].size(), is(3)) | ||||
|         assertThat(calledWithCredentials[0], allOf(hasEntry('type','file'), hasEntry('id','dockerConfigJsonCredentialsId'), hasEntry('env',['PIPER_dockerConfigJSON']))) | ||||
|  | ||||
|     } | ||||
| } | ||||
| @@ -127,6 +127,7 @@ public class CommonStepsTest extends BasePiperTest{ | ||||
|         'cloudFoundryCreateSpace', //implementing new golang pattern without fields | ||||
|         'cloudFoundryDeleteService', //implementing new golang pattern without fields | ||||
|         'cloudFoundryDeleteSpace', //implementing new golang pattern without fields | ||||
|         'cnbBuild', //implementing new golang pattern without fields | ||||
|         'durationMeasure', // only expects parameters via signature | ||||
|         'prepareDefaultValues', // special step (infrastructure) | ||||
|         'piperPipeline', // special step (infrastructure) | ||||
|   | ||||
							
								
								
									
										9
									
								
								vars/cnbBuild.groovy
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								vars/cnbBuild.groovy
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import groovy.transform.Field | ||||
|  | ||||
| @Field String STEP_NAME = getClass().getName() | ||||
| @Field String METADATA_FILE = 'metadata/cnbBuild.yaml' | ||||
|  | ||||
| void call(Map parameters = [:]) { | ||||
|     List credentials = [[type: 'file', id: 'dockerConfigJsonCredentialsId', env: ['PIPER_dockerConfigJSON']]] | ||||
|     piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user