github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/boot/boot.go (about)

     1  package boot
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  
     8  	"github.com/google/uuid"
     9  
    10  	"github.com/jenkins-x/jx/v2/pkg/versionstream"
    11  
    12  	"github.com/jenkins-x/jx/v2/pkg/boot"
    13  	v1 "k8s.io/api/core/v1"
    14  
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  
    17  	"github.com/jenkins-x/jx-logging/pkg/log"
    18  	"github.com/jenkins-x/jx/v2/pkg/cmd/helper"
    19  	"github.com/jenkins-x/jx/v2/pkg/cmd/namespace"
    20  	"github.com/jenkins-x/jx/v2/pkg/cmd/step/create"
    21  	"github.com/jenkins-x/jx/v2/pkg/config"
    22  	"github.com/jenkins-x/jx/v2/pkg/gits"
    23  	"github.com/jenkins-x/jx/v2/pkg/util"
    24  	"github.com/pkg/errors"
    25  
    26  	"github.com/spf13/cobra"
    27  
    28  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
    29  	"github.com/jenkins-x/jx/v2/pkg/cmd/templates"
    30  )
    31  
    32  // BootOptions options for the command
    33  type BootOptions struct {
    34  	*opts.CommonOptions
    35  
    36  	Dir          string
    37  	GitURL       string
    38  	GitRef       string
    39  	StartStep    string
    40  	EndStep      string
    41  	HelmLogLevel string
    42  
    43  	// The bootstrap URL for the version stream. Once we have a jx-requirements.yml files, we read that
    44  	VersionStreamURL string
    45  	// The bootstrap ref for the version stream. Once we have a jx-requirements.yml, we read that
    46  	VersionStreamRef string
    47  
    48  	// RequirementsFile provided by the user to override the default requirements file from repository
    49  	RequirementsFile string
    50  
    51  	AttemptRestore bool
    52  
    53  	// UpgradeGit if we want to automatically upgrade this boot clone if there have been changes since the current clone
    54  	NoUpgradeGit bool
    55  }
    56  
    57  var (
    58  	bootLong = templates.LongDesc(`
    59  		Boots up Jenkins X in a Kubernetes cluster using GitOps and a Jenkins X Pipeline
    60  
    61  		For more documentation see: [https://jenkins-x.io/docs/getting-started/setup/boot/](https://jenkins-x.io/docs/getting-started/setup/boot/)
    62  
    63  `)
    64  
    65  	bootExample = templates.Examples(`
    66  		# create a kubernetes cluster via Terraform or via jx
    67  		jx create cluster gke --skip-installation
    68  
    69  		# now lets boot up Jenkins X installing/upgrading whatever is needed
    70  		jx boot
    71  
    72  		# if we have already booted and just want to apply some environment changes without
    73          # re-applying ingress and so forth we can start at the environment step:
    74  		jx boot --start-step install-env
    75  `)
    76  )
    77  
    78  // NewCmdBoot creates the command
    79  func NewCmdBoot(commonOpts *opts.CommonOptions) *cobra.Command {
    80  	options := &BootOptions{
    81  		CommonOptions: commonOpts,
    82  	}
    83  	cmd := &cobra.Command{
    84  		Use:     "boot",
    85  		Aliases: []string{"bootstrap"},
    86  		Short:   "Boots up Jenkins X in a Kubernetes cluster using GitOps and a Jenkins X Pipeline",
    87  		Long:    bootLong,
    88  		Example: bootExample,
    89  		Run: func(cmd *cobra.Command, args []string) {
    90  			options.Cmd = cmd
    91  			options.Args = args
    92  			err := options.Run()
    93  			helper.CheckErr(err)
    94  		},
    95  	}
    96  
    97  	cmd.Flags().StringVarP(&options.Dir, "dir", "d", ".", "the directory to look for the Jenkins X Pipeline, requirements and charts")
    98  	cmd.Flags().StringVarP(&options.GitURL, "git-url", "u", "", "override the Git clone URL for the JX Boot source to start from, ignoring the versions stream. Normally specified with git-ref as well")
    99  	cmd.Flags().StringVarP(&options.GitRef, "git-ref", "", "", "override the Git ref for the JX Boot source to start from, ignoring the versions stream. Normally specified with git-url as well")
   100  	cmd.Flags().StringVarP(&options.VersionStreamURL, "versions-repo", "", config.DefaultVersionsURL, "the bootstrap URL for the versions repo. Once the boot config is cloned, the repo will be then read from the jx-requirements.yml")
   101  	cmd.Flags().StringVarP(&options.VersionStreamRef, "versions-ref", "", config.DefaultVersionsRef, "the bootstrap ref for the versions repo. Once the boot config is cloned, the repo will be then read from the jx-requirements.yml")
   102  	cmd.Flags().StringVarP(&options.StartStep, "start-step", "s", "", "the step in the pipeline to start from")
   103  	cmd.Flags().StringVarP(&options.EndStep, "end-step", "e", "", "the step in the pipeline to end at")
   104  	cmd.Flags().StringVarP(&options.HelmLogLevel, "helm-log", "v", "", "sets the helm logging level from 0 to 9. Passed into the helm CLI via the '-v' argument. Useful to diagnose helm related issues")
   105  	cmd.Flags().StringVarP(&options.RequirementsFile, "requirements", "r", "", "WARNING: this should only be used for the initial boot of a cluster: requirements file which will overwrite the default requirements file")
   106  	cmd.Flags().BoolVarP(&options.AttemptRestore, "attempt-restore", "a", false, "attempt to boot from an existing dev environment repository")
   107  	cmd.Flags().BoolVarP(&options.NoUpgradeGit, "no-update-git", "", false, "disables any attempt to update the local git clone if its old")
   108  
   109  	return cmd
   110  }
   111  
   112  // Run runs this command
   113  func (o *BootOptions) Run() error {
   114  	err := o.verifyClusterConnection()
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	o.overrideSteps()
   120  
   121  	if o.AttemptRestore {
   122  		err := o.restoreFromDevEnvRepo()
   123  		if err != nil {
   124  			return errors.Wrapf(err, "unable tp restore dev environment repo")
   125  		}
   126  	}
   127  
   128  	gitURL, gitRef := o.determineGitURLAndRef()
   129  	if gitURL == "" {
   130  		return util.MissingOption("git-url")
   131  	}
   132  
   133  	isBootClone, err := existingBootClone(o.Dir)
   134  	if err != nil {
   135  		return errors.Wrapf(err, "failed to check if this is an existing boot clone")
   136  	}
   137  
   138  	isGitRepo := o.isGitRepo(o.Dir)
   139  	if isGitRepo && !isBootClone {
   140  		return errors.Errorf("trying to execute 'jx boot' from a non requirements repo")
   141  	}
   142  
   143  	if !isBootClone {
   144  		gitInfo, err := gits.ParseGitURL(gitURL)
   145  		if err != nil {
   146  			return errors.Wrapf(err, "failed to parse git URL %s", gitURL)
   147  		}
   148  		dir := filepath.Join(o.Dir, gitInfo.Name)
   149  		cloneDir, err := o.createBootClone(gitURL, gitRef, dir)
   150  		if err != nil {
   151  			return errors.Wrapf(err, "unable to clone %s", gitURL)
   152  		}
   153  		o.Dir = cloneDir
   154  	}
   155  
   156  	requirements, requirementsFile, err := config.LoadRequirementsConfig(o.Dir, config.DefaultFailOnValidationError)
   157  	if err != nil {
   158  		return errors.Wrapf(err, "unable to load %s", config.RequirementsConfigFileName)
   159  	}
   160  
   161  	err = o.ConfigureCommonOptions(requirements)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	// lets report errors parsing this file after the check we are outside of a git clone
   167  	o.defaultVersionStream(requirements)
   168  
   169  	resolver, err := o.CreateVersionResolver(requirements.VersionStream.URL, requirements.VersionStream.Ref)
   170  	if err != nil {
   171  		return errors.Wrapf(err, "there was a problem creating a version resolver from versions stream repository %s and ref %s", requirements.VersionStream.URL, requirements.VersionStream.Ref)
   172  	}
   173  
   174  	// lets avoid trying to reset the current git clone to master if using NoUpgradeGit
   175  	if o.GitRef == "" && !o.NoUpgradeGit {
   176  		gitRef, err = o.determineGitRef(resolver, requirements, gitURL)
   177  		if err != nil {
   178  			return errors.Wrapf(err, "failed to determine git ref")
   179  		}
   180  	}
   181  
   182  	projectConfig, pipelineFile, err := config.LoadProjectConfig(o.Dir)
   183  	if err != nil {
   184  		return err
   185  	}
   186  
   187  	// check if the user has passed in a requirements file to a cluster which has already been provisioned.
   188  	if o.RequirementsFile != "" {
   189  		if err := o.checkIfProvidedRequirementsArePossiblyStale(); err != nil {
   190  			return errors.Wrapf(err, "loading from provided requirements file")
   191  		}
   192  	}
   193  
   194  	if err := o.overrideRequirements(gitURL); err != nil {
   195  		return errors.Wrap(err, "overwriting the default requirements")
   196  	}
   197  
   198  	// only update boot if the a GitRef has not been supplied
   199  	if o.GitRef == "" && !o.NoUpgradeGit {
   200  		err = o.updateBootCloneIfOutOfDate(gitRef)
   201  		if err != nil {
   202  			return err
   203  		}
   204  	}
   205  
   206  	err = o.verifyRequirements(requirements, requirementsFile)
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	log.Logger().Infof("Booting Jenkins X")
   212  
   213  	// now lets really boot
   214  	_, so := create.NewCmdStepCreateTaskAndOption(o.CommonOptions)
   215  	so.CloneDir = o.Dir
   216  	so.CloneDir = o.Dir
   217  	so.InterpretMode = true
   218  	so.NoReleasePrepare = true
   219  	so.StartStep = o.StartStep
   220  	so.EndStep = o.EndStep
   221  
   222  	so.AdditionalEnvVars = map[string]string{
   223  		"JX_NO_TILLER":                     "true",
   224  		boot.ConfigRepoURLEnvVarName:       gitURL,
   225  		boot.ConfigBaseRefEnvVarName:       gitRef,
   226  		boot.VersionsRepoURLEnvVarName:     requirements.VersionStream.URL,
   227  		boot.VersionsRepoBaseRefEnvVarName: requirements.VersionStream.Ref,
   228  	}
   229  	if o.NoUpgradeGit {
   230  		so.AdditionalEnvVars[boot.DisablePushUpdatesToDevEnvironment] = "true"
   231  	}
   232  	if requirements.Cluster.HelmMajorVersion == "3" {
   233  		so.AdditionalEnvVars["JX_HELM3"] = "true"
   234  	} else {
   235  		so.AdditionalEnvVars["JX_NO_TILLER"] = "true"
   236  	}
   237  	if o.HelmLogLevel != "" {
   238  		so.AdditionalEnvVars["JX_HELM_VERBOSE"] = o.HelmLogLevel
   239  	}
   240  
   241  	// Set the namespace in the pipeline
   242  	so.CommonOptions.SetDevNamespace(requirements.Cluster.Namespace)
   243  	// lets ensure the namespace is set in the jenkins-x.yml file
   244  	envVars := make([]v1.EnvVar, 0)
   245  	for _, e := range projectConfig.PipelineConfig.Pipelines.Release.Pipeline.Environment {
   246  		if e.Name == "DEPLOY_NAMESPACE" {
   247  			envVars = append(envVars, v1.EnvVar{
   248  				Name:  "DEPLOY_NAMESPACE",
   249  				Value: requirements.Cluster.Namespace,
   250  			})
   251  		} else {
   252  			envVars = append(envVars, e)
   253  		}
   254  	}
   255  	projectConfig.PipelineConfig.Pipelines.Release.Pipeline.Environment = envVars
   256  	err = projectConfig.SaveConfig(pipelineFile)
   257  	if err != nil {
   258  		return errors.Wrapf(err, "setting namespace in jenkins-x.yml")
   259  	}
   260  	so.VersionResolver = resolver
   261  
   262  	if o.BatchMode {
   263  		so.AdditionalEnvVars["JX_BATCH_MODE"] = "true"
   264  	}
   265  	err = so.Run()
   266  	if err != nil {
   267  		return errors.Wrapf(err, "failed to interpret pipeline file %s", pipelineFile)
   268  	}
   269  
   270  	log.Logger().Debugf("Using additional vars: %+v", so.AdditionalEnvVars)
   271  
   272  	// lets switch kubernetes context to it so the user can use `jx` commands immediately
   273  	no := &namespace.NamespaceOptions{}
   274  	no.CommonOptions = o.CommonOptions
   275  	no.Args = []string{requirements.Cluster.Namespace}
   276  	return no.Run()
   277  }
   278  
   279  func (o *BootOptions) isGitRepo(dir string) bool {
   280  	_, _, err := gits.GetGitInfoFromDirectory(dir, o.Git())
   281  	if err == nil {
   282  		return true
   283  	}
   284  	return false
   285  }
   286  
   287  func (o *BootOptions) determineGitURLAndRef() (string, string) {
   288  	gitURL, gitRef, err := gits.GetGitInfoFromDirectory(o.Dir, o.Git())
   289  	if err != nil {
   290  		log.Logger().Info("Creating boot config with defaults, as not in an existing boot directory with a git repository.")
   291  		gitURL = config.DefaultBootRepository
   292  		gitRef = config.DefaultVersionsRef
   293  	}
   294  
   295  	if o.GitURL != "" {
   296  		log.Logger().Infof("GitURL provided, overriding the current value: %s with %s", util.ColorInfo(gitURL), util.ColorInfo(o.GitURL))
   297  		gitURL = o.GitURL
   298  	}
   299  
   300  	if o.GitRef != "" {
   301  		log.Logger().Infof("GitRef provided, overriding the current value: %s with %s", util.ColorInfo(gitRef), util.ColorInfo(o.GitRef))
   302  		gitRef = o.GitRef
   303  	}
   304  	return gitURL, gitRef
   305  }
   306  
   307  func (o *BootOptions) createBootClone(bootConfigGitURL string, bootConfigGitRef string, cloneDir string) (string, error) {
   308  	info := util.ColorInfo
   309  	log.Logger().Infof("No Jenkins X pipeline file %s or no jx boot requirements file %s found. You are not running this command from inside a "+
   310  		"Jenkins X Boot git clone", info(config.ProjectConfigFileName), info(config.RequirementsConfigFileName))
   311  
   312  	if !o.BatchMode {
   313  		log.Logger().Infof("To continue we will clone %s @ %s to %s", info(bootConfigGitURL), info(bootConfigGitRef), info(cloneDir))
   314  
   315  		help := "A git clone of a Jenkins X Boot source repository is required for 'jx boot'"
   316  		message := "Do you want to clone the Jenkins X Boot Git repository?"
   317  		if answer, err := util.Confirm(message, true, help, o.GetIOFileHandles()); err != nil {
   318  			return "", errors.Wrapf(err, "unable to process user input")
   319  		} else if !answer {
   320  			return "", fmt.Errorf("please run this command again inside a git clone from a Jenkins X Boot repository")
   321  		}
   322  	}
   323  
   324  	bootCloneExists, err := util.DirExists(cloneDir)
   325  	if err != nil {
   326  		return "", errors.Wrapf(err, "unable to check whether directory %s exists", cloneDir)
   327  	}
   328  	if bootCloneExists {
   329  		return "", fmt.Errorf("cannot clone git repository to %s as the dir already exists", cloneDir)
   330  	}
   331  
   332  	log.Logger().Infof("Cloning %s @ %s to %s\n", info(bootConfigGitURL), info(bootConfigGitRef), info(cloneDir))
   333  
   334  	err = os.MkdirAll(cloneDir, util.DefaultWritePermissions)
   335  	if err != nil {
   336  		return "", errors.Wrapf(err, "failed to create directory: %s", cloneDir)
   337  	}
   338  
   339  	err = o.Git().Clone(bootConfigGitURL, cloneDir)
   340  	if err != nil {
   341  		return "", errors.Wrapf(err, "failed to clone git URL %s to directory: %s", bootConfigGitURL, cloneDir)
   342  	}
   343  
   344  	commitish, err := gits.FindTagForVersion(cloneDir, bootConfigGitRef, o.Git())
   345  	if err != nil {
   346  		log.Logger().Debugf(errors.Wrapf(err, "finding tag for %s", bootConfigGitRef).Error())
   347  	}
   348  
   349  	if commitish != "" {
   350  		err = o.Git().Reset(cloneDir, commitish, true)
   351  		if err != nil {
   352  			return "", errors.Wrapf(err, "setting HEAD to %s", commitish)
   353  		}
   354  	} else {
   355  		log.Logger().Debugf("fetching branch %s", bootConfigGitRef)
   356  		err = o.Git().FetchBranch(cloneDir, "origin", bootConfigGitRef)
   357  		if err != nil {
   358  			return "", errors.Wrapf(err, "fetching branch %s", bootConfigGitRef)
   359  		}
   360  
   361  		branchName := uuid.New().String()
   362  
   363  		err = o.Git().CreateBranchFrom(cloneDir, branchName, bootConfigGitRef)
   364  		if err != nil {
   365  			return "", errors.Wrapf(err, "create branch %s from %s", branchName, bootConfigGitRef)
   366  		}
   367  
   368  		err = o.Git().Checkout(cloneDir, branchName)
   369  		if err != nil {
   370  			return "", errors.Wrapf(err, "checkout branch %s", branchName)
   371  		}
   372  	}
   373  
   374  	cloneDir, err = filepath.Abs(cloneDir)
   375  	if err != nil {
   376  		return "", errors.Wrapf(err, "unable to determine absolute path for %s", cloneDir)
   377  	}
   378  	return cloneDir, nil
   379  }
   380  
   381  func (o *BootOptions) restoreFromDevEnvRepo() error {
   382  	url := o.determineDevEnvironmentUrl()
   383  	if url != "" {
   384  		cloned, dir, err := o.cloneDevEnvironment(url)
   385  		if err != nil {
   386  			return err
   387  		}
   388  		if cloned {
   389  			err = os.Chdir(dir)
   390  			if err != nil {
   391  				return errors.Wrapf(err, "failed to change into new directory: %s", dir)
   392  			}
   393  		} else {
   394  			log.Logger().Infof("failed to clone dev environment booting from %s", o.GitURL)
   395  		}
   396  	} else {
   397  		log.Logger().Infof("cannot determine dev environment url booting from %s", o.GitURL)
   398  	}
   399  	return nil
   400  }
   401  
   402  func (o *BootOptions) determineDevEnvironmentUrl() string {
   403  	gitProvider := os.Getenv("JX_VALUE_GITPROVIDER")
   404  	gitOwner := os.Getenv(config.RequirementEnvGitOwner)
   405  	clusterName := os.Getenv(config.RequirementClusterName)
   406  	if gitProvider != "" && gitOwner != "" && clusterName != "" {
   407  		repo := fmt.Sprintf("environment-%s-dev", clusterName)
   408  		repoName := o.Git().RepoName(gitOwner, repo)
   409  		url := fmt.Sprintf("https://%s.com/%s", gitProvider, repoName)
   410  		log.Logger().Infof("dev environment url is %s", url)
   411  		return url
   412  	}
   413  	return ""
   414  }
   415  
   416  func (o *BootOptions) cloneDevEnvironment(gitURL string) (bool, string, error) {
   417  	log.Logger().Infof("dev environment url specified %t ", o.AttemptRestore)
   418  	gitInfo, err := gits.ParseGitURL(gitURL)
   419  	if err != nil {
   420  		return false, "", errors.Wrapf(err, "failed to parse git URL %s", gitURL)
   421  	}
   422  
   423  	repo := gitInfo.Name
   424  	cloneDir := filepath.Join(o.Dir, repo)
   425  
   426  	err = os.MkdirAll(cloneDir, util.DefaultWritePermissions)
   427  	if err != nil {
   428  		return false, "", errors.Wrapf(err, "failed to create directory: %s", cloneDir)
   429  	}
   430  
   431  	err = o.Git().Clone(gitURL, cloneDir)
   432  	if err != nil {
   433  		log.Logger().Infof("failed to clone git URL %s to directory: %s", gitURL, cloneDir)
   434  		rmErr := os.RemoveAll(cloneDir)
   435  		if rmErr != nil {
   436  			log.Logger().Warnf("Unable to remove dev env directory")
   437  		}
   438  		return false, "", nil
   439  	}
   440  
   441  	return true, cloneDir, nil
   442  }
   443  
   444  func (o *BootOptions) updateBootCloneIfOutOfDate(gitRef string) error {
   445  	// Get the tag corresponding to the git ref.
   446  	commitish, err := gits.FindTagForVersion(o.Dir, gitRef, o.Git())
   447  	if err != nil {
   448  		log.Logger().Debugf(errors.Wrapf(err, "finding tag for %s", gitRef).Error())
   449  		commitish = fmt.Sprintf("%s/%s", "origin", gitRef)
   450  	}
   451  
   452  	// Check if the current HEAD is an ancestor of the tag.
   453  	isAncestor, err := o.Git().IsAncestor(o.Dir, "HEAD", commitish)
   454  	if err != nil {
   455  		log.Logger().Debugf(errors.Wrapf(err, "couldn't determine whether HEAD is an ancestor of %s", commitish).Error())
   456  	}
   457  
   458  	// Get the tag on the current boot clone HEAD, if any.
   459  	resolved, _, err := o.Git().Describe(o.Dir, true, "HEAD", "0", true)
   460  	if err != nil {
   461  		return errors.Wrap(err, "could not describe boot clone HEAD to find its tag with git describe HEAD --abbrev=0 --contains --always")
   462  	}
   463  
   464  	if resolved != commitish {
   465  		if isAncestor {
   466  			log.Logger().Infof("Local boot clone is out of date. It is based on %s, but the version stream is using %s. The clone will now be updated to %s.",
   467  				resolved, commitish, commitish)
   468  			log.Logger().Info("Stashing any changes made in local boot clone.")
   469  
   470  			err = o.Git().StashPush(o.Dir)
   471  			if err != nil {
   472  				return errors.WithStack(err)
   473  			}
   474  			err = o.Git().Reset(o.Dir, commitish, true)
   475  			if err != nil {
   476  				return errors.Wrapf(err, "Could not reset local boot clone to %s", commitish)
   477  			}
   478  
   479  			err = o.Git().StashPop(o.Dir)
   480  			if err != nil && !gits.IsNoStashEntriesError(err) { // Ignore no stashes as that's just because there was nothing to stash
   481  				return fmt.Errorf("Could not update local boot clone due to conflicts between local changes and %s.\n"+
   482  					"To fix this, resolve the conflicts listed below manually, run 'git reset HEAD', and run 'jx boot' again.\n%s",
   483  					commitish, gits.GetSimpleIndentedStashPopErrorMessage(err))
   484  			}
   485  		} else {
   486  			log.Logger().Infof("Current HEAD %s in %s is not an ancestor of %s, the boot config version from the version stream.\n"+
   487  				"Proceeding with current HEAD.", resolved, o.Dir, commitish)
   488  			return nil
   489  		}
   490  	}
   491  	return nil
   492  }
   493  
   494  func existingBootClone(dir string) (bool, error) {
   495  	pipelineExists, err := util.FileExists(filepath.Join(dir, config.ProjectConfigFileName))
   496  	if err != nil {
   497  		return false, err
   498  	}
   499  	requirementsExist, err := util.FileExists(filepath.Join(dir, config.RequirementsConfigFileName))
   500  	if err != nil {
   501  		return false, err
   502  	}
   503  	return requirementsExist && pipelineExists, nil
   504  }
   505  
   506  func (o *BootOptions) checkIfProvidedRequirementsArePossiblyStale() error {
   507  	_, devEnv := o.GetDevEnv()
   508  	if devEnv != nil {
   509  		log.Logger().Warnf("It seems you're passing a requirements file to cluster which has already been provisioned.")
   510  		log.Logger().Warnf("We recommend you update the %s file at %s, using the updates provided within your local %s file.",
   511  			config.RequirementsConfigFileName, devEnv.Spec.Source, o.RequirementsFile)
   512  		return errors.New("attempting to boot cluster using a requirements file which is possibly stale")
   513  	}
   514  	return nil
   515  }
   516  
   517  func (o *BootOptions) overrideRequirements(defaultBootConfigURL string) error {
   518  	requirements, requirementsFile, err := config.LoadRequirementsConfig(o.Dir, config.DefaultFailOnValidationError)
   519  	if err != nil {
   520  		return errors.Wrapf(err, "loading requirements from dir %q", o.Dir)
   521  	}
   522  
   523  	// overwrite the default requirements with provided requirements
   524  	if o.RequirementsFile != "" {
   525  		providedRequirements, err := config.LoadRequirementsConfigFile(o.RequirementsFile, config.DefaultFailOnValidationError)
   526  		if err != nil {
   527  			return errors.Wrapf(err, "loading requirements from file %q", o.RequirementsFile)
   528  		}
   529  		*requirements = *providedRequirements
   530  	}
   531  
   532  	o.defaultVersionStream(requirements)
   533  	if requirements.BootConfigURL == "" {
   534  		requirements.BootConfigURL = defaultBootConfigURL
   535  	}
   536  
   537  	if err := requirements.SaveConfig(requirementsFile); err != nil {
   538  		return errors.Wrapf(err, "saving the requirements into file %q", requirementsFile)
   539  	}
   540  
   541  	return nil
   542  }
   543  
   544  func (o *BootOptions) determineGitRef(resolver *versionstream.VersionResolver, requirements *config.RequirementsConfig, gitURL string) (string, error) {
   545  	// If the GitRef is not overridden and is set to it's default value then look up the version number
   546  	log.Logger().Infof("Attempting to resolve version for boot config %s from %s", util.ColorInfo(gitURL), util.ColorInfo(requirements.VersionStream.URL))
   547  	gitRef, err := resolver.ResolveGitVersion(gitURL)
   548  	if err != nil {
   549  		return "", errors.Wrapf(err, fmt.Sprintf("failed to resolve version for %s in version stream %s",
   550  			gitURL, requirements.VersionStream.URL))
   551  	}
   552  	if gitRef == "" {
   553  		log.Logger().Infof("no version for %s found in version stream %s, defaulting to %s",
   554  			util.ColorInfo(gitURL), util.ColorInfo(requirements.VersionStream.URL), util.ColorInfo("master"))
   555  		gitRef = "master"
   556  	}
   557  	return gitRef, nil
   558  }
   559  
   560  func (o *BootOptions) defaultVersionStream(requirements *config.RequirementsConfig) {
   561  	if requirements.VersionStream.URL == "" && requirements.VersionStream.Ref == "" {
   562  		requirements.VersionStream.URL = o.VersionStreamURL
   563  		requirements.VersionStream.Ref = o.VersionStreamRef
   564  	}
   565  	// If we still don't have a complete version stream ref then we better set to a default
   566  	if requirements.VersionStream.URL == "" || requirements.VersionStream.Ref == "" {
   567  		log.Logger().Warnf("Incomplete version stream reference %s @ %s", requirements.VersionStream.URL, requirements.VersionStream.Ref)
   568  		o.VersionStreamRef = config.DefaultVersionsRef
   569  		o.VersionStreamURL = config.DefaultVersionsURL
   570  		requirements.VersionStream.URL = o.VersionStreamURL
   571  		requirements.VersionStream.Ref = o.VersionStreamRef
   572  		log.Logger().Infof("Setting version stream reference to default %s @ %s", requirements.VersionStream.URL, requirements.VersionStream.Ref)
   573  	}
   574  }
   575  
   576  func (o *BootOptions) verifyRequirements(requirements *config.RequirementsConfig, requirementsFile string) error {
   577  	provider := requirements.Cluster.Provider
   578  	if provider == "" {
   579  		return config.MissingRequirement("provider", requirementsFile)
   580  	}
   581  	if requirements.Cluster.Namespace == "" {
   582  		return config.MissingRequirement("namespace", requirementsFile)
   583  	}
   584  	return nil
   585  }
   586  
   587  func (o *BootOptions) verifyClusterConnection() error {
   588  	client, ns, err := o.KubeClientAndNamespace()
   589  	if err != nil {
   590  		return errors.Wrapf(err, "Unable to get current kube client/namespace  You are not currently connected to a cluster, please connect to the cluster that you intend to %s\n"+
   591  			"Alternatively create a new cluster using %s", util.ColorInfo("jx boot"), util.ColorInfo("jx create cluster"))
   592  	}
   593  
   594  	_, err = client.CoreV1().Pods(ns).List(metav1.ListOptions{})
   595  	if err != nil {
   596  		return errors.Wrapf(err, "Unable to list pods. You are not currently connected to a cluster, please connect to the cluster that you intend to %s\n"+
   597  			"Alternatively create a new cluster using %s", util.ColorInfo("jx boot"), util.ColorInfo("jx create cluster"))
   598  	}
   599  
   600  	return nil
   601  }
   602  
   603  func (o *BootOptions) overrideSteps() {
   604  	if o.StartStep == "" {
   605  		startStep := os.Getenv("JX_BOOT_START_STEP")
   606  		if startStep != "" {
   607  			log.Logger().Debugf("Overriding start-step with env var: '%s'", startStep)
   608  			o.StartStep = startStep
   609  		}
   610  	}
   611  
   612  	if o.EndStep == "" {
   613  		endStep := os.Getenv("JX_BOOT_END_STEP")
   614  		if endStep != "" {
   615  			log.Logger().Debugf("Overriding end-step with env var: '%s'", endStep)
   616  			o.EndStep = endStep
   617  		}
   618  	}
   619  }