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, ¶ms.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 }