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  }