github.com/jenkins-x/jx/v2@v2.1.155/pkg/jenkinsfile/pipeline.go (about) 1 package jenkinsfile 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "reflect" 9 "strconv" 10 "strings" 11 "text/template" 12 13 "github.com/jenkins-x/jx-logging/pkg/log" 14 "github.com/jenkins-x/jx/v2/pkg/kube/naming" 15 "github.com/jenkins-x/jx/v2/pkg/tekton/syntax" 16 "github.com/jenkins-x/jx/v2/pkg/util" 17 "github.com/pkg/errors" 18 corev1 "k8s.io/api/core/v1" 19 "k8s.io/apimachinery/pkg/api/equality" 20 "sigs.k8s.io/yaml" 21 ) 22 23 const ( 24 // PipelineConfigFileName is the name of the pipeline configuration file 25 PipelineConfigFileName = "pipeline.yaml" 26 27 // PipelineTemplateFileName defines the jenkisnfile template used to generate the pipeline 28 PipelineTemplateFileName = "Jenkinsfile.tmpl" 29 30 // PipelineKindRelease represents a release pipeline triggered on merge to master (or a release branch) 31 PipelineKindRelease = "release" 32 33 // PipelineKindPullRequest represents a Pull Request pipeline 34 PipelineKindPullRequest = "pullrequest" 35 36 // PipelineKindFeature represents a pipeline on a feature branch 37 PipelineKindFeature = "feature" 38 39 // the modes of adding a step 40 41 // CreateStepModePre creates steps before any existing steps 42 CreateStepModePre = "pre" 43 44 // CreateStepModePost creates steps after the existing steps 45 CreateStepModePost = "post" 46 47 // CreateStepModeReplace replaces the existing steps with the new steps 48 CreateStepModeReplace = "replace" 49 ) 50 51 var ( 52 // PipelineKinds the possible values of pipeline 53 PipelineKinds = []string{PipelineKindRelease, PipelineKindPullRequest, PipelineKindFeature} 54 55 // PipelineLifecycleNames the possible names of lifecycles of pipeline 56 PipelineLifecycleNames = []string{"setup", "setversion", "prebuild", "build", "postbuild", "promote"} 57 58 // CreateStepModes the step creation modes 59 CreateStepModes = []string{CreateStepModePre, CreateStepModePost, CreateStepModeReplace} 60 ) 61 62 // Pipelines contains all the different kinds of pipeline for different branches 63 type Pipelines struct { 64 PullRequest *PipelineLifecycles `json:"pullRequest,omitempty"` 65 Release *PipelineLifecycles `json:"release,omitempty"` 66 Feature *PipelineLifecycles `json:"feature,omitempty"` 67 Post *PipelineLifecycle `json:"post,omitempty"` 68 Overrides []*syntax.PipelineOverride `json:"overrides,omitempty"` 69 Default *syntax.ParsedPipeline `json:"default,omitempty"` 70 } 71 72 // PipelineLifecycles defines the steps of a lifecycle section 73 type PipelineLifecycles struct { 74 Setup *PipelineLifecycle `json:"setup,omitempty"` 75 SetVersion *PipelineLifecycle `json:"setVersion,omitempty"` 76 PreBuild *PipelineLifecycle `json:"preBuild,omitempty"` 77 Build *PipelineLifecycle `json:"build,omitempty"` 78 PostBuild *PipelineLifecycle `json:"postBuild,omitempty"` 79 Promote *PipelineLifecycle `json:"promote,omitempty"` 80 Pipeline *syntax.ParsedPipeline `json:"pipeline,omitempty"` 81 } 82 83 // PipelineLifecycle defines the steps of a lifecycle section 84 type PipelineLifecycle struct { 85 Steps []*syntax.Step `json:"steps,omitempty"` 86 87 // PreSteps if using inheritance then invoke these steps before the base steps 88 PreSteps []*syntax.Step `json:"preSteps,omitempty"` 89 90 // Replace if using inheritance then replace steps from the base pipeline 91 Replace bool `json:"replace,omitempty"` 92 } 93 94 // NamedLifecycle a lifecycle and its name 95 type NamedLifecycle struct { 96 Name string 97 Lifecycle *PipelineLifecycle 98 } 99 100 // PipelineLifecycleArray an array of named lifecycle pointers 101 type PipelineLifecycleArray []NamedLifecycle 102 103 // PipelineExtends defines the extension (e.g. parent pipeline which is overloaded 104 type PipelineExtends struct { 105 Import string `json:"import,omitempty"` 106 File string `json:"file,omitempty"` 107 } 108 109 // ImportFile returns an ImportFile for the given extension 110 func (x *PipelineExtends) ImportFile() *ImportFile { 111 return &ImportFile{ 112 Import: x.Import, 113 File: x.File, 114 } 115 } 116 117 // PipelineConfig defines the pipeline configuration 118 type PipelineConfig struct { 119 Extends *PipelineExtends `json:"extends,omitempty"` 120 Agent *syntax.Agent `json:"agent,omitempty"` 121 Env []corev1.EnvVar `json:"env,omitempty"` 122 Environment string `json:"environment,omitempty"` 123 Pipelines Pipelines `json:"pipelines,omitempty"` 124 ContainerOptions *corev1.Container `json:"containerOptions,omitempty"` 125 } 126 127 // CreateJenkinsfileArguments contains the arguents to generate a Jenkinsfiles dynamically 128 type CreateJenkinsfileArguments struct { 129 ConfigFile string 130 TemplateFile string 131 OutputFile string 132 IsTekton bool 133 ClearContainerNames bool 134 } 135 136 // +k8s:deepcopy-gen=false 137 138 // CreatePipelineArguments contains the arguments to translate a build pack into a pipeline 139 type CreatePipelineArguments struct { 140 Lifecycles *PipelineLifecycles 141 PodTemplates map[string]*corev1.Pod 142 CustomImage string 143 DefaultImage string 144 WorkspaceDir string 145 GitHost string 146 GitName string 147 GitOrg string 148 ProjectID string 149 DockerRegistry string 150 DockerRegistryOrg string 151 KanikoImage string 152 UseKaniko bool 153 NoReleasePrepare bool 154 StepCounter int 155 } 156 157 // Validate validates all the arguments are set correctly 158 func (a *CreateJenkinsfileArguments) Validate() error { 159 if a.ConfigFile == "" { 160 return fmt.Errorf("Missing argument: ConfigFile") 161 } 162 if a.TemplateFile == "" { 163 return fmt.Errorf("Missing argument: TemplateFile") 164 } 165 if a.OutputFile == "" { 166 return fmt.Errorf("Missing argument: ReportName") 167 } 168 return nil 169 } 170 171 // Groovy returns the groovy expression for all of the lifecycles 172 func (a *PipelineLifecycles) Groovy() string { 173 return a.All().Groovy() 174 } 175 176 // All returns all lifecycles in order 177 func (a *PipelineLifecycles) All() PipelineLifecycleArray { 178 return []NamedLifecycle{ 179 {"setup", a.Setup}, 180 {"setversion", a.SetVersion}, 181 {"prebuild", a.PreBuild}, 182 {"build", a.Build}, 183 {"postbuild", a.PostBuild}, 184 {"promote", a.Promote}, 185 } 186 } 187 188 // AllButPromote returns all lifecycles but promote 189 func (a *PipelineLifecycles) AllButPromote() PipelineLifecycleArray { 190 return []NamedLifecycle{ 191 {"setup", a.Setup}, 192 {"setversion", a.SetVersion}, 193 {"prebuild", a.PreBuild}, 194 {"build", a.Build}, 195 {"postbuild", a.PostBuild}, 196 } 197 } 198 199 // RemoveWhenStatements removes any when conditions 200 func (a *PipelineLifecycles) RemoveWhenStatements(prow bool) { 201 for _, n := range a.All() { 202 v := n.Lifecycle 203 if v != nil { 204 v.RemoveWhenStatements(prow) 205 } 206 } 207 } 208 209 // GetLifecycle returns the pipeline lifecycle of the given name lazy creating on the fly if required 210 // or returns an error if the name is not valid 211 func (a *PipelineLifecycles) GetLifecycle(name string, lazyCreate bool) (*PipelineLifecycle, error) { 212 switch name { 213 case "setup": 214 if a.Setup == nil && lazyCreate { 215 a.Setup = &PipelineLifecycle{} 216 } 217 return a.Setup, nil 218 case "setversion": 219 if a.SetVersion == nil && lazyCreate { 220 a.SetVersion = &PipelineLifecycle{} 221 } 222 return a.SetVersion, nil 223 case "prebuild": 224 if a.PreBuild == nil && lazyCreate { 225 a.PreBuild = &PipelineLifecycle{} 226 } 227 return a.PreBuild, nil 228 case "build": 229 if a.Build == nil && lazyCreate { 230 a.Build = &PipelineLifecycle{} 231 } 232 return a.Build, nil 233 case "postbuild": 234 if a.PostBuild == nil && lazyCreate { 235 a.PostBuild = &PipelineLifecycle{} 236 } 237 return a.PostBuild, nil 238 case "promote": 239 if a.Promote == nil && lazyCreate { 240 a.Promote = &PipelineLifecycle{} 241 } 242 return a.Promote, nil 243 default: 244 return nil, fmt.Errorf("unknown pipeline lifecycle stage: %s", name) 245 } 246 } 247 248 // Groovy returns the groovy string for the lifecycles 249 func (s PipelineLifecycleArray) Groovy() string { 250 statements := []*util.Statement{} 251 for _, n := range s { 252 l := n.Lifecycle 253 if l != nil { 254 statements = append(statements, l.ToJenkinsfileStatements()...) 255 } 256 } 257 text := util.WriteJenkinsfileStatements(4, statements) 258 // lets remove the very last newline so its easier to compose in templates 259 text = strings.TrimSuffix(text, "\n") 260 return text 261 } 262 263 // Groovy returns the groovy expression for this lifecycle 264 func (l *NamedLifecycle) Groovy() string { 265 lifecycles := PipelineLifecycleArray([]NamedLifecycle{*l}) 266 return lifecycles.Groovy() 267 } 268 269 // PutAllEnvVars puts all the defined environment variables in the given map 270 func (l *NamedLifecycle) PutAllEnvVars(m map[string]string) { 271 if l.Lifecycle != nil { 272 for _, step := range l.Lifecycle.Steps { 273 step.PutAllEnvVars(m) 274 } 275 } 276 } 277 278 // Groovy returns the groovy expression for this lifecycle 279 func (l *PipelineLifecycle) Groovy() string { 280 nl := &NamedLifecycle{Name: "", Lifecycle: l} 281 return nl.Groovy() 282 } 283 284 // ToJenkinsfileStatements converts the lifecycle to one or more jenkinsfile statements 285 func (l *PipelineLifecycle) ToJenkinsfileStatements() []*util.Statement { 286 statements := []*util.Statement{} 287 for _, step := range l.Steps { 288 statements = append(statements, step.ToJenkinsfileStatements()...) 289 } 290 return statements 291 } 292 293 // RemoveWhenStatements removes any when conditions 294 func (l *PipelineLifecycle) RemoveWhenStatements(prow bool) { 295 l.PreSteps = removeWhenSteps(prow, l.PreSteps) 296 l.Steps = removeWhenSteps(prow, l.Steps) 297 } 298 299 // CreateStep creates the given step using the mode 300 func (l *PipelineLifecycle) CreateStep(mode string, step *syntax.Step) error { 301 err := step.Validate() 302 if err != nil { 303 return err 304 } 305 switch mode { 306 case CreateStepModePre: 307 l.PreSteps = append(l.PreSteps, step) 308 case CreateStepModePost: 309 l.Steps = append(l.Steps, step) 310 case CreateStepModeReplace: 311 l.Steps = []*syntax.Step{step} 312 l.Replace = true 313 default: 314 return fmt.Errorf("uknown create mode: %s", mode) 315 } 316 return nil 317 } 318 319 func removeWhenSteps(prow bool, steps []*syntax.Step) []*syntax.Step { 320 answer := []*syntax.Step{} 321 for _, step := range steps { 322 when := strings.TrimSpace(step.When) 323 if prow && when == "!prow" { 324 continue 325 } 326 if !prow && when == "prow" { 327 continue 328 } 329 step.Steps = removeWhenSteps(prow, step.Steps) 330 answer = append(answer, step) 331 } 332 return answer 333 } 334 335 // Extend extends these pipelines with the base pipeline 336 func (p *Pipelines) Extend(base *Pipelines) error { 337 p.PullRequest = ExtendPipelines("pullRequest", p.PullRequest, base.PullRequest, p.Overrides) 338 p.Release = ExtendPipelines("release", p.Release, base.Release, p.Overrides) 339 p.Feature = ExtendPipelines("feature", p.Feature, base.Feature, p.Overrides) 340 p.Post = ExtendLifecycle("", "post", p.Post, base.Post, p.Overrides) 341 return nil 342 } 343 344 // All returns all the lifecycles in this pipeline, some may be null 345 func (p *Pipelines) All() []*PipelineLifecycles { 346 return []*PipelineLifecycles{p.PullRequest, p.Feature, p.Release} 347 } 348 349 // AllMap returns all the lifecycles in this pipeline indexed by the pipeline name 350 func (p *Pipelines) AllMap() map[string]*PipelineLifecycles { 351 m := map[string]*PipelineLifecycles{} 352 if p.PullRequest != nil { 353 m[PipelineKindPullRequest] = p.PullRequest 354 } 355 if p.Feature != nil { 356 m[PipelineKindFeature] = p.Feature 357 } 358 if p.Release != nil { 359 m[PipelineKindRelease] = p.Release 360 } 361 return m 362 } 363 364 // defaultContainerAndDir defaults the container if none is being used 365 func (p *Pipelines) defaultContainerAndDir(container string, dir string) { 366 defaultContainerAndDir(container, dir, p.All()...) 367 } 368 369 // RemoveWhenStatements removes any prow or !prow statements 370 func (p *Pipelines) RemoveWhenStatements(prow bool) { 371 for _, l := range p.All() { 372 if l != nil { 373 l.RemoveWhenStatements(prow) 374 } 375 } 376 if p.Post != nil { 377 p.Post.RemoveWhenStatements(prow) 378 } 379 } 380 381 // GetPipeline returns the pipeline for the given name, creating if required if lazyCreate is true or returns an error if its not a valid name 382 func (p *Pipelines) GetPipeline(kind string, lazyCreate bool) (*PipelineLifecycles, error) { 383 switch kind { 384 case PipelineKindRelease: 385 if p.Release == nil && lazyCreate { 386 p.Release = &PipelineLifecycles{} 387 } 388 return p.Release, nil 389 case PipelineKindPullRequest: 390 if p.PullRequest == nil && lazyCreate { 391 p.PullRequest = &PipelineLifecycles{} 392 } 393 return p.PullRequest, nil 394 case PipelineKindFeature: 395 if p.Feature == nil && lazyCreate { 396 p.Feature = &PipelineLifecycles{} 397 } 398 return p.Feature, nil 399 default: 400 return nil, fmt.Errorf("no such pipeline kind: %s", kind) 401 } 402 } 403 404 func defaultContainerAndDir(container string, dir string, lifecycles ...*PipelineLifecycles) { 405 for _, l := range lifecycles { 406 if l != nil { 407 defaultLifecycleContainerAndDir(container, dir, l.All()) 408 } 409 } 410 } 411 412 func defaultLifecycleContainerAndDir(container string, dir string, lifecycles PipelineLifecycleArray) { 413 if container == "" && dir == "" { 414 return 415 } 416 for _, n := range lifecycles { 417 l := n.Lifecycle 418 if l != nil { 419 if dir != "" { 420 l.PreSteps = defaultDirAroundSteps(dir, l.PreSteps) 421 l.Steps = defaultDirAroundSteps(dir, l.Steps) 422 } 423 if container != "" { 424 l.PreSteps = defaultContainerAroundSteps(container, l.PreSteps) 425 l.Steps = defaultContainerAroundSteps(container, l.Steps) 426 } 427 } 428 } 429 } 430 431 func defaultContainerAroundSteps(container string, steps []*syntax.Step) []*syntax.Step { 432 if container == "" { 433 return steps 434 } 435 var containerStep *syntax.Step 436 result := []*syntax.Step{} 437 for _, step := range steps { 438 if step.GetImage() != "" { 439 result = append(result, step) 440 } else { 441 if containerStep == nil { 442 containerStep = &syntax.Step{ 443 Image: container, 444 } 445 result = append(result, containerStep) 446 } 447 containerStep.Steps = append(containerStep.Steps, step) 448 } 449 } 450 return result 451 } 452 453 func defaultDirAroundSteps(dir string, steps []*syntax.Step) []*syntax.Step { 454 if dir == "" { 455 return steps 456 } 457 var dirStep *syntax.Step 458 result := []*syntax.Step{} 459 for _, step := range steps { 460 if step.GetImage() != "" { 461 step.Steps = defaultDirAroundSteps(dir, step.Steps) 462 result = append(result, step) 463 } else if step.Dir != "" { 464 result = append(result, step) 465 } else { 466 if dirStep == nil { 467 dirStep = &syntax.Step{ 468 Dir: dir, 469 } 470 result = append(result, dirStep) 471 } 472 dirStep.Steps = append(dirStep.Steps, step) 473 } 474 } 475 return result 476 } 477 478 // LoadPipelineConfig returns the pipeline configuration 479 func LoadPipelineConfig(fileName string, resolver ImportFileResolver, isTekton bool, clearContainer bool) (*PipelineConfig, error) { 480 return LoadPipelineConfigAndMaybeValidate(fileName, resolver, isTekton, clearContainer, true) 481 } 482 483 // LoadPipelineConfigAndMaybeValidate returns the pipeline configuration, optionally after validating the YAML. 484 func LoadPipelineConfigAndMaybeValidate(fileName string, resolver ImportFileResolver, isTekton bool, clearContainer bool, skipYamlValidation bool) (*PipelineConfig, error) { 485 config := PipelineConfig{} 486 exists, err := util.FileExists(fileName) 487 if err != nil || !exists { 488 return &config, err 489 } 490 data, err := ioutil.ReadFile(fileName) 491 if err != nil { 492 return &config, errors.Wrapf(err, "Failed to load file %s", fileName) 493 } 494 if !skipYamlValidation { 495 validationErrors, err := util.ValidateYaml(&config, data) 496 if err != nil { 497 return &config, fmt.Errorf("failed to validate YAML file %s due to %s", fileName, err) 498 } 499 if len(validationErrors) > 0 { 500 return &config, fmt.Errorf("Validation failures in YAML file %s:\n%s", fileName, strings.Join(validationErrors, "\n")) 501 } 502 } 503 err = yaml.Unmarshal(data, &config) 504 if err != nil { 505 return &config, errors.Wrapf(err, "Failed to unmarshal file %s", fileName) 506 } 507 pipelines := &config.Pipelines 508 pipelines.RemoveWhenStatements(isTekton) 509 if clearContainer { 510 // lets force any agent for prow / jenkinsfile runner 511 config.Agent = clearContainerAndLabel(config.Agent) 512 } 513 config.PopulatePipelinesFromDefault() 514 if config.Extends == nil || config.Extends.File == "" { 515 config.defaultContainerAndDir() 516 return &config, nil 517 } 518 file := config.Extends.File 519 importModule := config.Extends.Import 520 if importModule != "" { 521 file, err = resolver(config.Extends.ImportFile()) 522 if err != nil { 523 return &config, errors.Wrapf(err, "Failed to resolve imports for file %s", fileName) 524 } 525 526 } else if !filepath.IsAbs(file) { 527 dir, _ := filepath.Split(fileName) 528 if dir != "" { 529 file = filepath.Join(dir, file) 530 } 531 } 532 exists, err = util.FileExists(file) 533 if err != nil { 534 return &config, errors.Wrapf(err, "base pipeline file does not exist %s", file) 535 } 536 if !exists { 537 return &config, fmt.Errorf("base pipeline file does not exist %s", file) 538 } 539 basePipeline, err := LoadPipelineConfig(file, resolver, isTekton, clearContainer) 540 if err != nil { 541 return &config, errors.Wrapf(err, "Failed to base pipeline file %s", file) 542 } 543 err = config.ExtendPipeline(basePipeline, clearContainer) 544 return &config, err 545 } 546 547 // PopulatePipelinesFromDefault sets the Release, PullRequest, and Feature pipelines, if unset, with the Default pipeline. 548 func (c *PipelineConfig) PopulatePipelinesFromDefault() { 549 if c != nil && c.Pipelines.Default != nil { 550 if c.Pipelines.Default.Agent == nil && c.Agent != nil { 551 c.Pipelines.Default.Agent = c.Agent.DeepCopyForParsedPipeline() 552 } 553 if c.Pipelines.Release == nil { 554 c.Pipelines.Release = &PipelineLifecycles{ 555 Pipeline: c.Pipelines.Default.DeepCopy(), 556 } 557 } 558 if c.Pipelines.PullRequest == nil { 559 c.Pipelines.PullRequest = &PipelineLifecycles{ 560 Pipeline: c.Pipelines.Default.DeepCopy(), 561 } 562 } 563 if c.Pipelines.Feature == nil { 564 c.Pipelines.Feature = &PipelineLifecycles{ 565 Pipeline: c.Pipelines.Default.DeepCopy(), 566 } 567 } 568 } 569 } 570 571 // clearContainerAndLabel wipes the label and container from an Agent, preserving the Dir if it exists. 572 func clearContainerAndLabel(agent *syntax.Agent) *syntax.Agent { 573 if agent != nil { 574 agent.Container = "" 575 agent.Image = "" 576 agent.Label = "" 577 578 return agent 579 } 580 return &syntax.Agent{} 581 } 582 583 // IsEmpty returns true if this configuration is empty 584 func (c *PipelineConfig) IsEmpty() bool { 585 empty := &PipelineConfig{} 586 return reflect.DeepEqual(empty, c) 587 } 588 589 // SaveConfig saves the configuration file to the given project directory 590 func (c *PipelineConfig) SaveConfig(fileName string) error { 591 data, err := yaml.Marshal(c) 592 if err != nil { 593 return err 594 } 595 return ioutil.WriteFile(fileName, data, util.DefaultWritePermissions) 596 } 597 598 // ExtendPipeline inherits this pipeline from the given base pipeline 599 func (c *PipelineConfig) ExtendPipeline(base *PipelineConfig, clearContainer bool) error { 600 if clearContainer { 601 c.Agent = clearContainerAndLabel(c.Agent) 602 base.Agent = clearContainerAndLabel(base.Agent) 603 } else { 604 if c.Agent == nil { 605 c.Agent = &syntax.Agent{} 606 } 607 if base.Agent == nil { 608 base.Agent = &syntax.Agent{} 609 } 610 if c.Agent.Label == "" { 611 c.Agent.Label = base.Agent.Label 612 } else if base.Agent.Label == "" && c.Agent.Label != "" { 613 base.Agent.Label = c.Agent.Label 614 } 615 if c.Agent.GetImage() == "" { 616 c.Agent.Image = base.Agent.GetImage() 617 } else if base.Agent.GetImage() == "" && c.Agent.GetImage() != "" { 618 base.Agent.Image = c.Agent.GetImage() 619 } 620 } 621 if c.Agent.Dir == "" { 622 c.Agent.Dir = base.Agent.Dir 623 } else if base.Agent.Dir == "" && c.Agent.Dir != "" { 624 base.Agent.Dir = c.Agent.Dir 625 } 626 mergedContainer, err := syntax.MergeContainers(base.ContainerOptions, c.ContainerOptions) 627 if err != nil { 628 return err 629 } 630 c.ContainerOptions = mergedContainer 631 base.defaultContainerAndDir() 632 c.defaultContainerAndDir() 633 c.Env = syntax.CombineEnv(c.Env, base.Env) 634 err = c.Pipelines.Extend(&base.Pipelines) 635 if err != nil { 636 return err 637 } 638 return nil 639 } 640 641 func (c *PipelineConfig) defaultContainerAndDir() { 642 if c.Agent != nil { 643 c.Pipelines.defaultContainerAndDir(c.Agent.GetImage(), c.Agent.Dir) 644 } 645 } 646 647 // GetAllEnvVars finds all the environment variables defined in all pipelines + steps with the first value we find 648 func (c *PipelineConfig) GetAllEnvVars() map[string]string { 649 answer := map[string]string{} 650 651 for _, pipeline := range c.Pipelines.All() { 652 if pipeline != nil { 653 for _, lifecycle := range pipeline.All() { 654 lifecycle.PutAllEnvVars(answer) 655 } 656 } 657 } 658 for _, env := range c.Env { 659 if env.Value != "" || answer[env.Name] == "" { 660 answer[env.Name] = env.Value 661 } 662 } 663 return answer 664 665 } 666 667 // ExtendPipelines extends the parent lifecycle with the base 668 func ExtendPipelines(pipelineName string, parent, base *PipelineLifecycles, overrides []*syntax.PipelineOverride) *PipelineLifecycles { 669 if base == nil { 670 return parent 671 } 672 if parent == nil { 673 parent = &PipelineLifecycles{} 674 } 675 l := &PipelineLifecycles{ 676 Setup: ExtendLifecycle(pipelineName, "setup", parent.Setup, base.Setup, overrides), 677 SetVersion: ExtendLifecycle(pipelineName, "setVersion", parent.SetVersion, base.SetVersion, overrides), 678 PreBuild: ExtendLifecycle(pipelineName, "preBuild", parent.PreBuild, base.PreBuild, overrides), 679 Build: ExtendLifecycle(pipelineName, "build", parent.Build, base.Build, overrides), 680 PostBuild: ExtendLifecycle(pipelineName, "postBuild", parent.PostBuild, base.PostBuild, overrides), 681 Promote: ExtendLifecycle(pipelineName, "promote", parent.Promote, base.Promote, overrides), 682 } 683 if parent.Pipeline != nil { 684 l.Pipeline = parent.Pipeline 685 } else if base.Pipeline != nil { 686 l.Pipeline = base.Pipeline 687 } 688 for _, override := range overrides { 689 if override.MatchesPipeline(pipelineName) { 690 // If no name, stage, or agent is specified, remove the whole pipeline. 691 if override.Name == "" && override.Stage == "" && override.Agent == nil && override.ContainerOptions == nil && len(override.Volumes) == 0 { 692 return &PipelineLifecycles{} 693 } 694 695 l.Pipeline = syntax.ApplyStepOverridesToPipeline(l.Pipeline, override) 696 } 697 } 698 return l 699 } 700 701 // ExtendLifecycle extends the lifecycle with the inherited base lifecycle 702 func ExtendLifecycle(pipelineName, stageName string, parent *PipelineLifecycle, base *PipelineLifecycle, overrides []*syntax.PipelineOverride) *PipelineLifecycle { 703 var lifecycle *PipelineLifecycle 704 if parent == nil { 705 lifecycle = base 706 } else if base == nil { 707 lifecycle = parent 708 } else if parent.Replace { 709 lifecycle = parent 710 } else { 711 steps := []*syntax.Step{} 712 steps = append(steps, parent.PreSteps...) 713 steps = append(steps, base.Steps...) 714 steps = append(steps, parent.Steps...) 715 lifecycle = &PipelineLifecycle{ 716 Steps: steps, 717 } 718 } 719 720 if lifecycle != nil { 721 for _, override := range overrides { 722 if override.MatchesPipeline(pipelineName) && override.MatchesStage(stageName) { 723 overriddenSteps := []*syntax.Step{} 724 725 // If a step name is specified on this override, override looking for that step. 726 if override.Name != "" { 727 for _, s := range lifecycle.Steps { 728 for _, o := range syntax.OverrideStep(*s, override) { 729 overriddenStep := o 730 overriddenSteps = append(overriddenSteps, &overriddenStep) 731 } 732 } 733 } else { 734 // If no step name was specified but there are steps, just replace all steps in the stage/lifecycle, 735 // or add the new steps before/after the existing steps in the stage/lifecycle 736 if steps := override.AsStepsSlice(); len(steps) > 0 { 737 if override.Type == nil || *override.Type == syntax.StepOverrideReplace { 738 overriddenSteps = append(overriddenSteps, steps...) 739 } else if *override.Type == syntax.StepOverrideBefore { 740 overriddenSteps = append(overriddenSteps, steps...) 741 overriddenSteps = append(overriddenSteps, lifecycle.Steps...) 742 } else if *override.Type == syntax.StepOverrideAfter { 743 overriddenSteps = append(overriddenSteps, lifecycle.Steps...) 744 overriddenSteps = append(overriddenSteps, override.Steps...) 745 } 746 } 747 // If there aren't any steps as well as no step name, then we're removing all steps from this stage/lifecycle, 748 // so do nothing. =) 749 } 750 lifecycle.Steps = overriddenSteps 751 } 752 } 753 } 754 755 return lifecycle 756 } 757 758 // GenerateJenkinsfile generates the jenkinsfile 759 func (a *CreateJenkinsfileArguments) GenerateJenkinsfile(resolver ImportFileResolver) error { 760 err := a.Validate() 761 if err != nil { 762 return err 763 } 764 config, err := LoadPipelineConfig(a.ConfigFile, resolver, a.IsTekton, a.ClearContainerNames) 765 if err != nil { 766 return err 767 } 768 769 templateFile := a.TemplateFile 770 771 data, err := ioutil.ReadFile(templateFile) 772 if err != nil { 773 return errors.Wrapf(err, "failed to load template %s", templateFile) 774 } 775 776 t, err := template.New("myJenkinsfile").Parse(string(data)) 777 if err != nil { 778 return errors.Wrapf(err, "failed to parse template %s", templateFile) 779 } 780 outFile := a.OutputFile 781 outDir, _ := filepath.Split(outFile) 782 if outDir != "" { 783 err = os.MkdirAll(outDir, util.DefaultWritePermissions) 784 if err != nil { 785 return errors.Wrapf(err, "failed to make directory %s when creating Jenkinsfile %s", outDir, outFile) 786 } 787 } 788 file, err := os.Create(outFile) 789 if err != nil { 790 return errors.Wrapf(err, "failed to create file %s", outFile) 791 } 792 defer file.Close() 793 794 err = t.Execute(file, config) 795 if err != nil { 796 return errors.Wrapf(err, "failed to write file %s", outFile) 797 } 798 return nil 799 } 800 801 // createPipelineSteps translates a step into one or more steps that can be used in jenkins-x.yml pipeline syntax. 802 func (c *PipelineConfig) createPipelineSteps(step *syntax.Step, prefixPath string, args CreatePipelineArguments) ([]syntax.Step, int) { 803 steps := []syntax.Step{} 804 805 containerName := c.Agent.GetImage() 806 807 if step.GetImage() != "" { 808 containerName = step.GetImage() 809 } 810 811 dir := args.WorkspaceDir 812 813 if step.Dir != "" { 814 dir = step.Dir 815 } 816 817 if step.GetCommand() != "" { 818 if containerName == "" { 819 containerName = args.DefaultImage 820 log.Logger().Warnf("No 'agent.container' specified in the pipeline configuration so defaulting to use: %s", containerName) 821 } 822 823 s := syntax.Step{} 824 args.StepCounter++ 825 prefix := prefixPath 826 if prefix != "" { 827 prefix += "-" 828 } 829 stepName := step.Name 830 if stepName == "" { 831 stepName = "step" + strconv.Itoa(1+args.StepCounter) 832 } 833 s.Name = prefix + stepName 834 s.Command = replaceCommandText(step) 835 if args.CustomImage != "" { 836 s.Image = args.CustomImage 837 } else { 838 s.Image = containerName 839 } 840 841 s.Dir = dir 842 s.Env = step.Env 843 steps = append(steps, s) 844 } else if step.Loop != nil { 845 // Just copy in the loop step without altering it. 846 // TODO: We don't get magic around image resolution etc, but we avoid naming collisions that result otherwise. 847 steps = append(steps, *step) 848 } 849 for _, s := range step.Steps { 850 // TODO add child prefix? 851 childPrefixPath := prefixPath 852 args.WorkspaceDir = dir 853 nestedSteps, nestedCounter := c.createPipelineSteps(s, childPrefixPath, args) 854 args.StepCounter = nestedCounter 855 steps = append(steps, nestedSteps...) 856 } 857 return steps, args.StepCounter 858 } 859 860 // replaceCommandText lets remove any escaped "\$" stuff in the pipeline library 861 // and replace any use of the VERSION file with using the VERSION env var 862 func replaceCommandText(step *syntax.Step) string { 863 answer := strings.Replace(step.GetFullCommand(), "\\$", "$", -1) 864 865 // lets replace the old way of setting versions 866 answer = strings.Replace(answer, "export VERSION=`cat VERSION` && ", "", 1) 867 answer = strings.Replace(answer, "export VERSION=$PREVIEW_VERSION && ", "", 1) 868 869 for _, text := range []string{"$(cat VERSION)", "$(cat ../VERSION)", "$(cat ../../VERSION)"} { 870 answer = strings.Replace(answer, text, "${VERSION}", -1) 871 } 872 return answer 873 } 874 875 // createStageForBuildPack generates the Task for a build pack 876 func (c *PipelineConfig) createStageForBuildPack(args CreatePipelineArguments) (*syntax.Stage, int, error) { 877 if args.Lifecycles == nil { 878 return nil, args.StepCounter, errors.New("generatePipeline: no lifecycles") 879 } 880 881 // lets generate the pipeline using the build packs 882 container := "" 883 if c.Agent != nil { 884 container = c.Agent.GetImage() 885 886 } 887 if args.CustomImage != "" { 888 container = args.CustomImage 889 } 890 if container == "" { 891 container = args.DefaultImage 892 } 893 894 steps := []syntax.Step{} 895 for _, n := range args.Lifecycles.All() { 896 l := n.Lifecycle 897 if l == nil { 898 continue 899 } 900 if !args.NoReleasePrepare && n.Name == "setversion" { 901 continue 902 } 903 904 for _, s := range l.Steps { 905 newSteps, newCounter := c.createPipelineSteps(s, n.Name, args) 906 args.StepCounter = newCounter 907 steps = append(steps, newSteps...) 908 } 909 } 910 911 stage := &syntax.Stage{ 912 Name: syntax.DefaultStageNameForBuildPack, 913 Agent: &syntax.Agent{ 914 Image: container, 915 }, 916 Steps: steps, 917 } 918 919 return stage, args.StepCounter, nil 920 } 921 922 // CreatePipelineForBuildPack translates a set of lifecycles into a full pipeline. 923 func (c *PipelineConfig) CreatePipelineForBuildPack(args CreatePipelineArguments) (*syntax.ParsedPipeline, int, error) { 924 args.GitOrg = naming.ToValidName(strings.ToLower(args.GitOrg)) 925 args.GitName = naming.ToValidName(strings.ToLower(args.GitName)) 926 args.DockerRegistryOrg = strings.ToLower(args.DockerRegistryOrg) 927 928 stage, newCounter, err := c.createStageForBuildPack(args) 929 if err != nil { 930 return nil, args.StepCounter, errors.Wrapf(err, "Failed to generate stage from build pack") 931 } 932 933 parsed := &syntax.ParsedPipeline{ 934 Stages: []syntax.Stage{*stage}, 935 } 936 937 // If agent.container is specified, use that for default container configuration for step images. 938 containerName := c.Agent.GetImage() 939 if containerName != "" { 940 if args.PodTemplates != nil && args.PodTemplates[containerName] != nil { 941 podTemplate := args.PodTemplates[containerName] 942 container := podTemplate.Spec.Containers[0] 943 if !equality.Semantic.DeepEqual(container, corev1.Container{}) { 944 container.Name = "" 945 container.Command = nil 946 container.Args = nil 947 container.Image = "" 948 container.WorkingDir = "" 949 container.Stdin = false 950 container.TTY = false 951 if parsed.Options == nil { 952 parsed.Options = &syntax.RootOptions{} 953 } 954 parsed.Options.ContainerOptions = &container 955 for _, v := range podTemplate.Spec.Volumes { 956 parsed.Options.Volumes = append(parsed.Options.Volumes, &corev1.Volume{ 957 Name: v.Name, 958 VolumeSource: v.VolumeSource, 959 }) 960 } 961 } 962 } 963 } 964 965 return parsed, newCounter, nil 966 }