github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/tekton/metapipeline/metapipeline.go (about)

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