github.com/jenkins-x/jx/v2@v2.1.155/pkg/tekton/metapipeline/metapipeline.go (about)

     1  package metapipeline
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  
     7  	"github.com/jenkins-x/jx/v2/pkg/kube"
     8  	"k8s.io/apimachinery/pkg/api/resource"
     9  
    10  	jenkinsv1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    11  	"github.com/jenkins-x/jx-api/pkg/client/clientset/versioned"
    12  	"github.com/jenkins-x/jx/v2/pkg/apps"
    13  	"github.com/jenkins-x/jx/v2/pkg/gits"
    14  	"github.com/jenkins-x/jx/v2/pkg/tekton"
    15  	"github.com/jenkins-x/jx/v2/pkg/tekton/syntax"
    16  	"github.com/jenkins-x/jx/v2/pkg/util"
    17  
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  
    20  	"github.com/jenkins-x/jx-logging/pkg/log"
    21  	"github.com/pkg/errors"
    22  	pipelineapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
    23  	corev1 "k8s.io/api/core/v1"
    24  )
    25  
    26  const (
    27  	// MetaPipelineStageName is the name used for the single stage used within a metapipeline
    28  	MetaPipelineStageName = "meta-pipeline"
    29  
    30  	// mergePullRefsStepName is the meta pipeline step name for merging all pull refs into the workspace
    31  	mergePullRefsStepName = "merge-pull-refs"
    32  	// createEffectivePipelineStepName is the meta pipeline step name for the generation of the effective jenkins-x pipeline config
    33  	createEffectivePipelineStepName = "create-effective-pipeline"
    34  	// createTektonCRDsStepName is the meta pipeline step name for the Tekton CRD creation
    35  	createTektonCRDsStepName = "create-tekton-crds"
    36  
    37  	tektonBaseDir = "/workspace"
    38  
    39  	mavenSettingsSecretName = "jenkins-maven-settings" // #nosec
    40  	mavenSettingsMount      = "/root/.m2/"
    41  )
    42  
    43  // CRDCreationParameters are the parameters needed to create the Tekton CRDs
    44  type CRDCreationParameters struct {
    45  	Namespace           string
    46  	Context             string
    47  	PipelineName        string
    48  	PipelineKind        PipelineKind
    49  	BuildNumber         string
    50  	GitInfo             gits.GitRepository
    51  	BranchIdentifier    string
    52  	PullRef             PullRef
    53  	SourceDir           string
    54  	PodTemplates        map[string]*corev1.Pod
    55  	ServiceAccount      string
    56  	Labels              map[string]string
    57  	EnvVars             map[string]string
    58  	DefaultImage        string
    59  	Apps                []jenkinsv1.App
    60  	VersionsDir         string
    61  	UseBranchAsRevision bool
    62  	NoReleasePrepare    bool
    63  }
    64  
    65  // createMetaPipelineCRDs creates the Tekton CRDs needed to execute the meta pipeline.
    66  // The meta pipeline is responsible to checkout the source repository at the right revision, allows Jenkins-X Apps
    67  // to modify the pipeline (via modifying the configuration on the file system) and finally triggering the actual
    68  // build pipeline.
    69  // An error is returned in case the creation of the Tekton CRDs fails.
    70  func createMetaPipelineCRDs(params CRDCreationParameters) (*tekton.CRDWrapper, error) {
    71  	parsedPipeline, err := createPipeline(params)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	labels, err := buildLabels(params)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	crdParams := syntax.CRDsFromPipelineParams{
    82  		PipelineIdentifier: params.PipelineName,
    83  		BuildIdentifier:    params.BuildNumber,
    84  		Namespace:          params.Namespace,
    85  		PodTemplates:       params.PodTemplates,
    86  		VersionsDir:        params.VersionsDir,
    87  		SourceDir:          params.SourceDir,
    88  		Labels:             labels,
    89  		DefaultImage:       params.DefaultImage,
    90  		InterpretMode:      false,
    91  	}
    92  	pipeline, tasks, structure, err := parsedPipeline.GenerateCRDs(crdParams)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	revision := params.PullRef.BaseSHA()
    98  	if revision == "" {
    99  		revision = params.PullRef.BaseBranch()
   100  	}
   101  	resources := []*pipelineapi.PipelineResource{tekton.GenerateSourceRepoResource(params.PipelineName, &params.GitInfo, revision)}
   102  	run := tekton.CreatePipelineRun(resources, pipeline.Name, pipeline.APIVersion, labels, params.ServiceAccount, nil, nil, nil, nil)
   103  
   104  	tektonCRDs, err := tekton.NewCRDWrapper(pipeline, tasks, resources, structure, run)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	return tektonCRDs, nil
   110  }
   111  
   112  // getExtendingApps returns the list of apps which are installed in the cluster registered for extending the pipeline.
   113  // An app registers its interest in extending the pipeline by having the 'pipeline-extension' label set.
   114  func getExtendingApps(jxClient versioned.Interface, namespace string) ([]jenkinsv1.App, error) {
   115  	listOptions := metav1.ListOptions{}
   116  	listOptions.LabelSelector = fmt.Sprintf(apps.AppTypeLabel+" in (%s)", apps.PipelineExtension)
   117  	appsList, err := jxClient.JenkinsV1().Apps(namespace).List(listOptions)
   118  	if err != nil {
   119  		return nil, errors.Wrap(err, "error retrieving pipeline contributor apps")
   120  	}
   121  	return appsList.Items, nil
   122  }
   123  
   124  // createPipeline builds the parsed/typed pipeline which servers as source for the Tekton CRD creation.
   125  func createPipeline(params CRDCreationParameters) (*syntax.ParsedPipeline, error) {
   126  	steps, err := buildSteps(params)
   127  	if err != nil {
   128  		return nil, errors.Wrap(err, "unable to create app extending pipeline steps")
   129  	}
   130  
   131  	stage := syntax.Stage{
   132  		Name:  MetaPipelineStageName,
   133  		Steps: steps,
   134  		Agent: &syntax.Agent{
   135  			Image: determineDefaultStepImage(params.DefaultImage),
   136  		},
   137  		Options: &syntax.StageOptions{
   138  			RootOptions: &syntax.RootOptions{
   139  				Volumes: []*corev1.Volume{{
   140  					Name: mavenSettingsSecretName,
   141  					VolumeSource: corev1.VolumeSource{
   142  						Secret: &corev1.SecretVolumeSource{
   143  							SecretName: mavenSettingsSecretName,
   144  						},
   145  					},
   146  				}},
   147  				ContainerOptions: &corev1.Container{
   148  					Resources: corev1.ResourceRequirements{
   149  						Limits: corev1.ResourceList{
   150  							"cpu":    resource.MustParse("0.8"),
   151  							"memory": resource.MustParse("512Mi"),
   152  						},
   153  						Requests: corev1.ResourceList{
   154  							"cpu":    resource.MustParse("0.4"),
   155  							"memory": resource.MustParse("256Mi"),
   156  						},
   157  					},
   158  					VolumeMounts: []corev1.VolumeMount{{
   159  						Name:      mavenSettingsSecretName,
   160  						MountPath: mavenSettingsMount,
   161  					}},
   162  				},
   163  			},
   164  		},
   165  	}
   166  
   167  	parsedPipeline := &syntax.ParsedPipeline{
   168  		Stages: []syntax.Stage{stage},
   169  	}
   170  
   171  	env := buildEnvParams(params)
   172  	parsedPipeline.AddContainerEnvVarsToPipeline(env)
   173  
   174  	return parsedPipeline, nil
   175  }
   176  
   177  // buildSteps builds the meta pipeline steps.
   178  // The tasks of the meta pipeline are:
   179  // 1) make sure the right commits are merged
   180  // 2) create the effective pipeline and write it to disk
   181  // 3) one step for each extending app
   182  // 4) create Tekton CRDs for the meta pipeline
   183  func buildSteps(params CRDCreationParameters) ([]syntax.Step, error) {
   184  	var steps []syntax.Step
   185  
   186  	// 1)
   187  	step := stepMergePullRefs(params.PullRef)
   188  	steps = append(steps, step)
   189  
   190  	// 2)
   191  	step = stepEffectivePipeline(params)
   192  	steps = append(steps, step)
   193  
   194  	// 3)
   195  	log.Logger().Debugf("creating pipeline steps for extending apps")
   196  	for _, app := range params.Apps {
   197  		if app.Spec.PipelineExtension == nil {
   198  			log.Logger().Warnf("Skipping app %s in meta pipeline. It contains label %s with value %s, but does not contain PipelineExtension fields.", app.Name, apps.AppTypeLabel, apps.PipelineExtension)
   199  			continue
   200  		}
   201  
   202  		extension := app.Spec.PipelineExtension
   203  		step := syntax.Step{
   204  			Name:      extension.Name,
   205  			Image:     extension.Image,
   206  			Command:   extension.Command,
   207  			Arguments: extension.Args,
   208  		}
   209  
   210  		log.Logger().Debugf("App %s contributes with step %s", app.Name, util.PrettyPrint(step))
   211  		steps = append(steps, step)
   212  	}
   213  
   214  	// 4)
   215  	step = stepCreateTektonCRDs(params)
   216  	steps = append(steps, step)
   217  
   218  	return steps, nil
   219  }
   220  
   221  func stepMergePullRefs(pullRef PullRef) syntax.Step {
   222  	// we only need to run the merge step in case there is anything to merge
   223  	// Tekton has at this stage the base branch already checked out
   224  	if len(pullRef.pullRequests) == 0 {
   225  		return stepSkip(mergePullRefsStepName, "Nothing to merge")
   226  	}
   227  
   228  	args := []string{"--verbose", "--baseBranch", pullRef.BaseBranch(), "--baseSHA", pullRef.BaseSHA()}
   229  	for _, pr := range pullRef.pullRequests {
   230  		args = append(args, "--sha", pr.MergeSHA)
   231  	}
   232  
   233  	step := syntax.Step{
   234  		Name:      mergePullRefsStepName,
   235  		Comment:   "Pipeline step merging pull refs",
   236  		Command:   "jx step git merge",
   237  		Arguments: args,
   238  	}
   239  	return step
   240  }
   241  
   242  func stepEffectivePipeline(params CRDCreationParameters) syntax.Step {
   243  	args := []string{"--output-dir", "."}
   244  	if params.Context != "" {
   245  		args = append(args, "--context", params.Context)
   246  	}
   247  
   248  	for _, e := range buildEnvParams(params) {
   249  		args = append(args, fmt.Sprintf("--env %s=%s", e.Name, e.Value))
   250  	}
   251  
   252  	step := syntax.Step{
   253  		Name:      createEffectivePipelineStepName,
   254  		Comment:   "Pipeline step creating the effective pipeline configuration",
   255  		Command:   "jx step syntax effective",
   256  		Arguments: args,
   257  	}
   258  	return step
   259  }
   260  
   261  func stepCreateTektonCRDs(params CRDCreationParameters) syntax.Step {
   262  	args := []string{"--clone-dir", filepath.Join(tektonBaseDir, params.SourceDir)}
   263  	args = append(args, "--kind", params.PipelineKind.String())
   264  	for _, pr := range params.PullRef.PullRequests() {
   265  		args = append(args, "--pr-number", pr.ID)
   266  		// there might be a batch build building multiple PRs, in which case we just use the first in this case
   267  		break
   268  	}
   269  	args = append(args, "--service-account", params.ServiceAccount)
   270  	args = append(args, "--source", params.SourceDir)
   271  	args = append(args, "--branch", params.BranchIdentifier)
   272  	args = append(args, "--build-number", params.BuildNumber)
   273  	if params.Context != "" {
   274  		args = append(args, "--context", params.Context)
   275  	}
   276  	if params.UseBranchAsRevision {
   277  		args = append(args, "--branch-as-revision")
   278  	}
   279  	if params.NoReleasePrepare {
   280  		args = append(args, "--no-release-prepare")
   281  	}
   282  	for k, v := range params.Labels {
   283  		args = append(args, "--label", fmt.Sprintf("%s=%s", k, v))
   284  	}
   285  
   286  	step := syntax.Step{
   287  		Name:      createTektonCRDsStepName,
   288  		Comment:   "Pipeline step to create the Tekton CRDs for the actual pipeline run",
   289  		Command:   "jx step create task",
   290  		Arguments: args,
   291  	}
   292  	return step
   293  }
   294  
   295  func stepSkip(stepName string, msg string) syntax.Step {
   296  	skipMsg := fmt.Sprintf("SKIP %s: %s", stepName, msg)
   297  	step := syntax.Step{
   298  		Name:      stepName,
   299  		Comment:   skipMsg,
   300  		Command:   "echo",
   301  		Arguments: []string{fmt.Sprintf("'%s'", skipMsg)},
   302  	}
   303  	return step
   304  }
   305  
   306  func determineDefaultStepImage(defaultImage string) string {
   307  	if defaultImage != "" {
   308  		return defaultImage
   309  	}
   310  
   311  	return syntax.DefaultContainerImage
   312  }
   313  
   314  // buildEnvParams creates a set of environment variables we want to set on the meta pipeline as well as on the
   315  // build pipeline.
   316  // It first builds a list of variables based on the CRDCreationParameters and then appends any custom env variables
   317  // given through params.EnvVars
   318  func buildEnvParams(params CRDCreationParameters) []corev1.EnvVar {
   319  	var envVars []corev1.EnvVar
   320  
   321  	envVars = append(envVars, corev1.EnvVar{
   322  		Name:  "BUILD_NUMBER",
   323  		Value: params.BuildNumber,
   324  	})
   325  
   326  	envVars = append(envVars, corev1.EnvVar{
   327  		Name:  "PIPELINE_KIND",
   328  		Value: params.PipelineKind.String(),
   329  	})
   330  
   331  	envVars = append(envVars, corev1.EnvVar{
   332  		Name:  "PULL_REFS",
   333  		Value: params.PullRef.String(),
   334  	})
   335  
   336  	context := params.Context
   337  	if context != "" {
   338  		envVars = append(envVars, corev1.EnvVar{
   339  			Name:  "PIPELINE_CONTEXT",
   340  			Value: context,
   341  		})
   342  	}
   343  
   344  	gitInfo := params.GitInfo
   345  	envVars = append(envVars, corev1.EnvVar{
   346  		Name:  "SOURCE_URL",
   347  		Value: gitInfo.URL,
   348  	})
   349  
   350  	owner := gitInfo.Organisation
   351  	if owner != "" {
   352  		envVars = append(envVars, corev1.EnvVar{
   353  			Name:  "REPO_OWNER",
   354  			Value: owner,
   355  		})
   356  	}
   357  
   358  	repo := gitInfo.Name
   359  	if repo != "" {
   360  		envVars = append(envVars, corev1.EnvVar{
   361  			Name:  "REPO_NAME",
   362  			Value: repo,
   363  		})
   364  
   365  		// lets keep the APP_NAME environment variable we need for previews
   366  		envVars = append(envVars, corev1.EnvVar{
   367  			Name:  "APP_NAME",
   368  			Value: repo,
   369  		})
   370  	}
   371  
   372  	branch := params.BranchIdentifier
   373  	if branch != "" {
   374  		envVars = append(envVars, corev1.EnvVar{
   375  			Name:  util.EnvVarBranchName,
   376  			Value: branch,
   377  		})
   378  	}
   379  
   380  	if owner != "" && repo != "" && branch != "" {
   381  		jobName := fmt.Sprintf("%s/%s/%s", owner, repo, branch)
   382  		envVars = append(envVars, corev1.EnvVar{
   383  			Name:  "JOB_NAME",
   384  			Value: jobName,
   385  		})
   386  	}
   387  
   388  	customEnvVars := buildEnvVars(params.EnvVars)
   389  	for _, v := range customEnvVars {
   390  		// only append if not yes explicitly set
   391  		if kube.GetSliceEnvVar(envVars, v.Name) == nil {
   392  			envVars = append(envVars, v)
   393  		}
   394  	}
   395  
   396  	log.Logger().WithField("env", util.PrettyPrint(envVars)).Debug("meta pipeline env variables")
   397  	return envVars
   398  }
   399  
   400  func buildLabels(params CRDCreationParameters) (map[string]string, error) {
   401  	labels := map[string]string{}
   402  	labels[tekton.LabelOwner] = params.GitInfo.Organisation
   403  	labels[tekton.LabelRepo] = params.GitInfo.Name
   404  	labels[tekton.LabelBranch] = params.BranchIdentifier
   405  	if params.Context != "" {
   406  		labels[tekton.LabelContext] = params.Context
   407  	}
   408  	labels[tekton.LabelBuild] = params.BuildNumber
   409  	labels[tekton.LabelType] = tekton.MetaPipeline.String()
   410  
   411  	return util.MergeMaps(labels, params.Labels), nil
   412  }
   413  
   414  func buildEnvVars(customEnvVars map[string]string) []corev1.EnvVar {
   415  	var envVars []corev1.EnvVar
   416  
   417  	for key, value := range customEnvVars {
   418  		envVars = append(envVars, corev1.EnvVar{
   419  			Name:  key,
   420  			Value: value,
   421  		})
   422  	}
   423  
   424  	return envVars
   425  }