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