github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/kube/env.go (about)

     1  package kube
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"os/user"
     8  	"path/filepath"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/olli-ai/jx/v2/pkg/jenkinsfile"
    14  
    15  	"github.com/ghodss/yaml"
    16  	"github.com/pkg/errors"
    17  
    18  	v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    19  	"github.com/jenkins-x/jx-api/pkg/client/clientset/versioned"
    20  	"github.com/jenkins-x/jx-logging/pkg/log"
    21  	"github.com/olli-ai/jx/v2/pkg/auth"
    22  	"github.com/olli-ai/jx/v2/pkg/config"
    23  	"github.com/olli-ai/jx/v2/pkg/gits"
    24  	"github.com/olli-ai/jx/v2/pkg/util"
    25  	survey "gopkg.in/AlecAivazis/survey.v1"
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/client-go/kubernetes"
    29  )
    30  
    31  var useForkForEnvGitRepo = false
    32  
    33  // ResolveChartMuseumURLFn used to resolve the chart repository URL if using remote environments
    34  type ResolveChartMuseumURLFn func() (string, error)
    35  
    36  // CreateEnvironmentSurvey creates a Survey on the given environment using the default options
    37  // from the CLI
    38  func CreateEnvironmentSurvey(batchMode bool, authConfigSvc auth.ConfigService, devEnv *v1.Environment, data *v1.Environment,
    39  	config *v1.Environment, update bool, forkEnvGitURL string, ns string, jxClient versioned.Interface, kubeClient kubernetes.Interface, envDir string,
    40  	gitRepoOptions *gits.GitRepositoryOptions, helmValues config.HelmValuesConfig, prefix string, git gits.Gitter, chartMusemFn ResolveChartMuseumURLFn, handles util.IOFileHandles) (gits.GitProvider, error) {
    41  	surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err)
    42  	name := data.Name
    43  	createMode := name == ""
    44  	if createMode {
    45  		if config.Name != "" {
    46  			err := ValidNameOption(OptionName, config.Name)
    47  			if err != nil {
    48  				return nil, err
    49  			}
    50  			if !update {
    51  				err = ValidateEnvironmentDoesNotExist(jxClient, ns, config.Name)
    52  				if err != nil {
    53  					return nil, err
    54  				}
    55  			}
    56  			data.Name = config.Name
    57  		} else {
    58  			if batchMode {
    59  				return nil, fmt.Errorf("environment name cannot be empty. Use --name option.")
    60  			}
    61  
    62  			validator := func(val interface{}) error {
    63  				err := ValidateName(val)
    64  				if err != nil {
    65  					return err
    66  				}
    67  				str, ok := val.(string)
    68  				if !ok {
    69  					return fmt.Errorf("Expected string value")
    70  				}
    71  				v := ValidateEnvironmentDoesNotExist(jxClient, ns, str)
    72  				return v
    73  			}
    74  
    75  			q := &survey.Input{
    76  				Message: "Name:",
    77  				Help:    "The Environment name must be unique, lower case and a valid DNS name",
    78  			}
    79  			err := survey.AskOne(q, &data.Name, validator, surveyOpts)
    80  			if err != nil {
    81  				return nil, err
    82  			}
    83  		}
    84  	}
    85  	if string(config.Spec.Kind) != "" {
    86  		data.Spec.Kind = config.Spec.Kind
    87  	} else {
    88  		if string(data.Spec.Kind) == "" {
    89  			data.Spec.Kind = v1.EnvironmentKindTypePermanent
    90  		}
    91  	}
    92  	if config.Spec.Label != "" {
    93  		data.Spec.Label = config.Spec.Label
    94  	} else {
    95  		defaultValue := data.Spec.Label
    96  		if defaultValue == "" {
    97  			defaultValue = strings.Title(data.Name)
    98  		}
    99  		q := &survey.Input{
   100  			Message: "Label:",
   101  			Default: defaultValue,
   102  			Help:    "The Environment label is a person friendly descriptive text like 'Staging' or 'Production'",
   103  		}
   104  		err := survey.AskOne(q, &data.Spec.Label, survey.Required, surveyOpts)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  	}
   109  	if config.Spec.Namespace != "" {
   110  		err := ValidNameOption(OptionNamespace, config.Spec.Namespace)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  		data.Spec.Namespace = config.Spec.Namespace
   115  	} else {
   116  		defaultValue := data.Spec.Namespace
   117  		if defaultValue == "" {
   118  			// lets use the namespace as a team name
   119  			defaultValue = data.Namespace
   120  			if defaultValue == "" {
   121  				defaultValue = ns
   122  			}
   123  			if data.Name != "" {
   124  				if defaultValue == "" {
   125  					defaultValue = data.Name
   126  				} else {
   127  					defaultValue += "-" + data.Name
   128  				}
   129  			}
   130  		}
   131  		if batchMode {
   132  			data.Spec.Namespace = defaultValue
   133  		} else {
   134  			q := &survey.Input{
   135  				Message: "Namespace:",
   136  				Default: defaultValue,
   137  				Help:    "The Kubernetes namespace name to use for this Environment",
   138  			}
   139  			err := survey.AskOne(q, &data.Spec.Namespace, ValidateName, surveyOpts)
   140  			if err != nil {
   141  				return nil, err
   142  			}
   143  		}
   144  	}
   145  
   146  	if helmValues.ExposeController.Config.Domain == "" {
   147  		ic, err := GetIngressConfig(kubeClient, ns)
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  
   152  		if batchMode {
   153  			log.Logger().Infof("Running in batch mode and no domain flag used so defaulting to team domain %s", ic.Domain)
   154  			helmValues.ExposeController.Config.Domain = ic.Domain
   155  		} else {
   156  			q := &survey.Input{
   157  				Message: "Domain:",
   158  				Default: ic.Domain,
   159  				Help:    "Domain to expose ingress endpoints.  Example: jenkinsx.io, leave blank if no appplications are to be exposed via ingress rules",
   160  			}
   161  			err := survey.AskOne(q, &helmValues.ExposeController.Config.Domain, nil, surveyOpts)
   162  			if err != nil {
   163  				return nil, err
   164  			}
   165  		}
   166  	}
   167  
   168  	data.Spec.RemoteCluster = config.Spec.RemoteCluster
   169  	if !batchMode {
   170  		var err error
   171  		data.Spec.RemoteCluster, err = util.Confirm("Environment in separate cluster to Dev Environment:",
   172  			data.Spec.RemoteCluster, " Is this Environment going to be in a different cluster to the Development environment. For help on Multi Cluster support see: https://jenkins-x.io/getting-started/multi-cluster/", handles)
   173  		if err != nil {
   174  			return nil, err
   175  		}
   176  	}
   177  	if config.Spec.Cluster != "" {
   178  		data.Spec.Cluster = config.Spec.Cluster
   179  	} else {
   180  		if data.Spec.RemoteCluster {
   181  			// lets not show the UI for this if users specify the namespace via arguments
   182  			if !createMode || config.Spec.Namespace == "" {
   183  				defaultValue := data.Spec.Cluster
   184  				if batchMode {
   185  					data.Spec.Cluster = defaultValue
   186  				} else {
   187  					q := &survey.Input{
   188  						Message: "Cluster URL:",
   189  						Default: defaultValue,
   190  						Help:    "The Kubernetes cluster URL to use to host this Environment. You can leave this blank for now.",
   191  					}
   192  					// TODO validate/transform to match valid kubnernetes cluster syntax
   193  					err := survey.AskOne(q, &data.Spec.Cluster, nil, surveyOpts)
   194  					if err != nil {
   195  						return nil, err
   196  					}
   197  				}
   198  			}
   199  		}
   200  	}
   201  	if string(config.Spec.PromotionStrategy) != "" {
   202  		data.Spec.PromotionStrategy = config.Spec.PromotionStrategy
   203  	} else {
   204  		promoteValues := []string{
   205  			string(v1.PromotionStrategyTypeAutomatic),
   206  			string(v1.PromotionStrategyTypeManual),
   207  			string(v1.PromotionStrategyTypeNever),
   208  		}
   209  		defaultValue := string(data.Spec.PromotionStrategy)
   210  		if defaultValue == "" {
   211  			defaultValue = string(v1.PromotionStrategyTypeAutomatic)
   212  		}
   213  		q := &survey.Select{
   214  			Message: "Promotion Strategy:",
   215  			Options: promoteValues,
   216  			Default: defaultValue,
   217  			Help:    "Whether we promote to this Environment automatically, manually or never",
   218  		}
   219  		textValue := ""
   220  		err := survey.AskOne(q, &textValue, survey.Required, surveyOpts)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		if textValue != "" {
   225  			data.Spec.PromotionStrategy = v1.PromotionStrategyType(textValue)
   226  		}
   227  	}
   228  	if string(data.Spec.PromotionStrategy) == "" {
   229  		data.Spec.PromotionStrategy = v1.PromotionStrategyTypeAutomatic
   230  	}
   231  	if config.Spec.Order != 0 {
   232  		data.Spec.Order = config.Spec.Order
   233  	} else {
   234  		order := data.Spec.Order
   235  		if order == 0 {
   236  			// TODO should we generate an order to default to last one?
   237  			order = 100
   238  		}
   239  		defaultValue := util.Int32ToA(order)
   240  		q := &survey.Input{
   241  			Message: "Order:",
   242  			Default: defaultValue,
   243  			Help:    "This number is used to sort Environments in sequential order, lowest first",
   244  		}
   245  		textValue := ""
   246  		err := survey.AskOne(q, &textValue, survey.Required, surveyOpts)
   247  		if err != nil {
   248  			return nil, err
   249  		}
   250  		if textValue != "" {
   251  			i, err := util.AtoInt32(textValue)
   252  			if err != nil {
   253  				return nil, fmt.Errorf("Failed to convert input '%s' to number: %s", textValue, err)
   254  			}
   255  			data.Spec.Order = i
   256  		}
   257  	}
   258  	if batchMode && gitRepoOptions.Owner == "" {
   259  		devEnvGitOwner, err := GetDevEnvGitOwner(jxClient)
   260  		if err != nil {
   261  			return nil, fmt.Errorf("Failed to get default Git owner for repos: %s", err)
   262  		}
   263  		if devEnvGitOwner != "" {
   264  			gitRepoOptions.Owner = devEnvGitOwner
   265  		} else {
   266  			gitRepoOptions.Owner = gitRepoOptions.Username
   267  		}
   268  		log.Logger().Infof("Using %s environment git owner in batch mode.", util.ColorInfo(gitRepoOptions.Owner))
   269  	}
   270  	_, gitProvider, err := CreateEnvGitRepository(batchMode, authConfigSvc, devEnv, data, config, forkEnvGitURL, envDir, gitRepoOptions, helmValues, prefix, git, chartMusemFn, handles)
   271  	return gitProvider, err
   272  }
   273  
   274  // CreateEnvGitRepository creates the git repository for the given Environment
   275  func CreateEnvGitRepository(batchMode bool, authConfigSvc auth.ConfigService, devEnv *v1.Environment, data *v1.Environment, config *v1.Environment, forkEnvGitURL string, envDir string, gitRepoOptions *gits.GitRepositoryOptions, helmValues config.HelmValuesConfig, prefix string, git gits.Gitter, chartMusemFn ResolveChartMuseumURLFn, handles util.IOFileHandles) (*gits.GitRepository, gits.GitProvider, error) {
   276  	var gitProvider gits.GitProvider
   277  	var repo *gits.GitRepository
   278  	surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err)
   279  	createRepo := false
   280  	if config.Spec.Source.URL != "" {
   281  		data.Spec.Source.URL = config.Spec.Source.URL
   282  	} else {
   283  		showURLEdit := devEnv.Spec.TeamSettings.UseGitOps
   284  		if data.Spec.Source.URL == "" && !showURLEdit {
   285  			if devEnv.Spec.TeamSettings.AskOnCreate {
   286  				confirm := &survey.Confirm{
   287  					Message: "Would you like to use GitOps to manage this environment? :",
   288  					Default: false,
   289  				}
   290  				err := survey.AskOne(confirm, &showURLEdit, nil, surveyOpts)
   291  				if err != nil {
   292  					return repo, nil, errors.Wrap(err, "asking enable GitOps question")
   293  				}
   294  			} else {
   295  				showURLEdit = true
   296  			}
   297  		}
   298  		if showURLEdit {
   299  			if data.Spec.Source.URL == "" {
   300  				if batchMode {
   301  					createRepo = true
   302  				} else {
   303  					confirm := &survey.Confirm{
   304  						Message: fmt.Sprintf("We will now create a Git repository to store your %s environment, ok? :", data.Name),
   305  						Default: true,
   306  					}
   307  					err := survey.AskOne(confirm, &createRepo, nil, surveyOpts)
   308  					if err != nil {
   309  						return repo, nil, errors.Wrapf(err, "asking to create the git repository %q", data.Name)
   310  					}
   311  				}
   312  
   313  				if createRepo {
   314  					showURLEdit = false
   315  					var err error
   316  					repo, gitProvider, err = DoCreateEnvironmentGitRepo(batchMode, authConfigSvc, data, forkEnvGitURL, envDir, gitRepoOptions, helmValues, prefix, git, chartMusemFn, handles)
   317  					if err != nil {
   318  						return repo, gitProvider, errors.Wrap(err, "creating environment git repository")
   319  					}
   320  					data.Spec.Source.URL = repo.CloneURL
   321  				}
   322  			} else {
   323  				showURLEdit = true
   324  			}
   325  			if showURLEdit && !batchMode {
   326  				q := &survey.Input{
   327  					Message: "Git URL for the Environment source code:",
   328  					Default: data.Spec.Source.URL,
   329  					Help:    "The git clone URL for the Environment's Helm charts source code and custom configuration",
   330  				}
   331  				err := survey.AskOne(q, &data.Spec.Source.URL, survey.Required, surveyOpts)
   332  				if err != nil {
   333  					return repo, nil, errors.Wrap(err, "asking for environment git clone URL")
   334  				}
   335  			}
   336  		}
   337  	}
   338  	if config.Spec.Source.Ref != "" {
   339  		data.Spec.Source.Ref = config.Spec.Source.Ref
   340  	} else {
   341  		if data.Spec.Source.URL != "" || data.Spec.Source.Ref != "" {
   342  			if batchMode {
   343  				createRepo = true
   344  			} else {
   345  				defaultBranch := data.Spec.Source.Ref
   346  				if defaultBranch == "" {
   347  					defaultBranch = "master"
   348  				}
   349  				q := &survey.Input{
   350  					Message: "Git branch for the Environment source code:",
   351  					Default: defaultBranch,
   352  					Help:    "The Git release branch in the Environments Git repository used to store Helm charts source code and custom configuration",
   353  				}
   354  				err := survey.AskOne(q, &data.Spec.Source.Ref, nil, surveyOpts)
   355  				if err != nil {
   356  					return repo, nil, errors.Wrap(err, "asking git branch for environment source")
   357  				}
   358  			}
   359  		}
   360  	}
   361  	return repo, gitProvider, nil
   362  }
   363  
   364  // DoCreateEnvironmentGitRepo actually creates the git repository for the environment
   365  func DoCreateEnvironmentGitRepo(batchMode bool, authConfigSvc auth.ConfigService, env *v1.Environment, forkEnvGitURL string,
   366  	environmentsDir string, gitRepoOptions *gits.GitRepositoryOptions, helmValues config.HelmValuesConfig, prefix string,
   367  	git gits.Gitter, chartMuseumFn ResolveChartMuseumURLFn, handles util.IOFileHandles) (*gits.GitRepository, gits.GitProvider, error) {
   368  	defaultRepoName := fmt.Sprintf("environment-%s-%s", prefix, env.Name)
   369  	details, err := gits.PickNewGitRepository(batchMode, authConfigSvc, defaultRepoName, gitRepoOptions, nil, nil, git, handles)
   370  	if err != nil {
   371  		return nil, nil, errors.Wrap(err, "picking new git repository for environment")
   372  	}
   373  	org := details.Organisation
   374  
   375  	repoName := details.RepoName
   376  	owner := org
   377  	if owner == "" {
   378  		owner = details.User.Username
   379  	}
   380  	envDir := filepath.Join(environmentsDir, owner)
   381  	provider := details.GitProvider
   382  
   383  	repo, err := provider.GetRepository(owner, repoName)
   384  	if err == nil {
   385  		log.Logger().Infof("Git repository %s/%s already exists", util.ColorInfo(owner), util.ColorInfo(repoName))
   386  
   387  		if env.Spec.RemoteCluster {
   388  			log.Logger().Infof("git repository %s is remote so not modifying it", util.ColorInfo(repo.HTMLURL))
   389  			return repo, provider, nil
   390  		}
   391  
   392  		// if the repo already exists then lets just modify it if required
   393  		dir, err := util.CreateUniqueDirectory(envDir, details.RepoName, util.MaximumNewDirectoryAttempts)
   394  		if err != nil {
   395  			return nil, nil, errors.Wrap(err, "creating unique directory for environment repo")
   396  		}
   397  		pushGitURL, err := git.CreateAuthenticatedURL(repo.CloneURL, details.User)
   398  		if err != nil {
   399  			return nil, nil, errors.Wrap(err, "creating push URL for environment repo")
   400  		}
   401  		err = git.Clone(pushGitURL, dir)
   402  		if err != nil {
   403  			return nil, nil, errors.Wrapf(err, "cloning environment from %q into %q", pushGitURL, dir)
   404  		}
   405  		err = ModifyNamespace(handles.Out, dir, env, git, chartMuseumFn)
   406  		if err != nil {
   407  			return nil, nil, errors.Wrap(err, "modifying environment namespace")
   408  		}
   409  		err = addValues(handles.Out, dir, helmValues, git)
   410  		if err != nil {
   411  			return nil, nil, errors.Wrap(err, "adding helm values to the environment")
   412  		}
   413  		err = git.PushMaster(dir)
   414  		if err != nil {
   415  			return nil, nil, errors.Wrap(err, "pushing environment master branch")
   416  		}
   417  		log.Logger().Infof("Pushed Git repository to %s\n\n", util.ColorInfo(repo.HTMLURL))
   418  	} else {
   419  		log.Logger().Infof("Creating Git repository %s/%s\n", util.ColorInfo(owner), util.ColorInfo(repoName))
   420  
   421  		if forkEnvGitURL != "" {
   422  			gitInfo, err := gits.ParseGitURL(forkEnvGitURL)
   423  			if err != nil {
   424  				return nil, nil, errors.Wrapf(err, "parsing forked environment git URL %q", forkEnvGitURL)
   425  			}
   426  			originalOrg := gitInfo.Organisation
   427  			originalRepo := gitInfo.Name
   428  			if useForkForEnvGitRepo && gitInfo.IsGitHub() && provider.IsGitHub() && originalOrg != "" && originalRepo != "" {
   429  				// lets try fork the repository and rename it
   430  				repo, err := provider.ForkRepository(originalOrg, originalRepo, org)
   431  				if err != nil {
   432  					return nil, nil, fmt.Errorf("failed to fork GitHub repo %s/%s to organisation %s due to %s",
   433  						originalOrg, originalRepo, org, err)
   434  				}
   435  				if repoName != originalRepo {
   436  					repo, err = provider.RenameRepository(owner, originalRepo, repoName)
   437  					if err != nil {
   438  						return nil, nil, fmt.Errorf("failed to rename GitHub repo %s/%s to organisation %s due to %s",
   439  							originalOrg, originalRepo, repoName, err)
   440  					}
   441  				}
   442  				log.Logger().Infof("Forked Git repository to %s\n\n", util.ColorInfo(repo.HTMLURL))
   443  
   444  				dir, err := util.CreateUniqueDirectory(envDir, repoName, util.MaximumNewDirectoryAttempts)
   445  				if err != nil {
   446  					return nil, nil, errors.Wrapf(err, "creating unique dir to fork environment repository %q", envDir)
   447  				}
   448  				err = git.Clone(repo.CloneURL, dir)
   449  				if err != nil {
   450  					return nil, nil, errors.Wrapf(err, "cloning the environment %q", repo.CloneURL)
   451  				}
   452  				err = git.SetRemoteURL(dir, "upstream", forkEnvGitURL)
   453  				if err != nil {
   454  					return nil, nil, errors.Wrapf(err, "setting remote upstream %q in forked environment repo", forkEnvGitURL)
   455  				}
   456  				err = git.PullUpstream(dir)
   457  				if err != nil {
   458  					return nil, nil, errors.Wrap(err, "pulling upstream of forked environment repository")
   459  				}
   460  				err = ModifyNamespace(handles.Out, dir, env, git, chartMuseumFn)
   461  				if err != nil {
   462  					return nil, nil, errors.Wrap(err, "modifying namespace of forked environment")
   463  				}
   464  				err = addValues(handles.Out, dir, helmValues, git)
   465  				if err != nil {
   466  					return nil, nil, errors.Wrap(err, "adding helm values to the forked environment repo")
   467  				}
   468  				err = git.Push(dir, "origin", false, "HEAD")
   469  				if err != nil {
   470  					return nil, nil, errors.Wrapf(err, "pushing forked environment dir %q", dir)
   471  				}
   472  				return repo, provider, nil
   473  			}
   474  		}
   475  
   476  		// default to forking the URL if possible...
   477  		repo, err = details.CreateRepository()
   478  		if err != nil {
   479  			return nil, nil, errors.Wrap(err, "creating the repository")
   480  		}
   481  
   482  		if forkEnvGitURL != "" {
   483  			// now lets clone the fork and push it...
   484  			dir, err := util.CreateUniqueDirectory(envDir, details.RepoName, util.MaximumNewDirectoryAttempts)
   485  			if err != nil {
   486  				return nil, nil, errors.Wrap(err, "create unique directory for environment fork clone")
   487  			}
   488  			err = git.Clone(forkEnvGitURL, dir)
   489  			if err != nil {
   490  				return nil, nil, errors.Wrapf(err, "cloning the forked environment %q into %q", forkEnvGitURL, dir)
   491  			}
   492  			pushGitURL, err := git.CreateAuthenticatedURL(repo.CloneURL, details.User)
   493  			if err != nil {
   494  				return nil, nil, errors.Wrapf(err, "creating the push URL for %q", repo.CloneURL)
   495  			}
   496  			err = git.AddRemote(dir, "upstream", forkEnvGitURL)
   497  			if err != nil {
   498  				return nil, nil, errors.Wrapf(err, "adding remote %q to forked env clone", forkEnvGitURL)
   499  			}
   500  			err = git.UpdateRemote(dir, pushGitURL)
   501  			if err != nil {
   502  				return nil, nil, errors.Wrapf(err, "updating remote %q", pushGitURL)
   503  			}
   504  			err = ModifyNamespace(handles.Out, dir, env, git, chartMuseumFn)
   505  			if err != nil {
   506  				return nil, nil, errors.Wrapf(err, "modifying dev environment namespace")
   507  			}
   508  			err = addValues(handles.Out, dir, helmValues, git)
   509  			if err != nil {
   510  				return nil, nil, errors.Wrap(err, "adding helm values into environment git repository")
   511  			}
   512  			err = git.PushMaster(dir)
   513  			if err != nil {
   514  				return nil, nil, errors.Wrap(err, "push forked environment git repository")
   515  			}
   516  			log.Logger().Infof("Pushed Git repository to %s\n\n", util.ColorInfo(repo.HTMLURL))
   517  		}
   518  	}
   519  	return repo, provider, nil
   520  }
   521  
   522  // GetDevEnvTeamSettings gets the team settings from the specified namespace.
   523  func GetDevEnvTeamSettings(jxClient versioned.Interface, ns string) (*v1.TeamSettings, error) {
   524  	devEnv, err := GetDevEnvironment(jxClient, ns)
   525  	if err != nil {
   526  		log.Logger().Errorf("Error loading team settings. %v", err)
   527  		return nil, err
   528  	}
   529  	if devEnv != nil {
   530  		return &devEnv.Spec.TeamSettings, nil
   531  	}
   532  	return nil, fmt.Errorf("unable to find development environment in %s to get team settings", ns)
   533  }
   534  
   535  // GetDevEnvGitOwner gets the default GitHub owner/organisation to use for Environment repos. This takes the setting
   536  // from the 'jx' Dev Env to get the one that was selected at installation time.
   537  func GetDevEnvGitOwner(jxClient versioned.Interface) (string, error) {
   538  	adminDevEnv, err := GetDevEnvironment(jxClient, "jx")
   539  	if err != nil {
   540  		log.Logger().Errorf("Error loading team settings. %v", err)
   541  		return "", err
   542  	}
   543  	if adminDevEnv != nil {
   544  		return adminDevEnv.Spec.TeamSettings.EnvOrganisation, nil
   545  	}
   546  	return "", errors.New("Unable to find development environment in 'jx' to take git owner from")
   547  }
   548  
   549  // ModifyNamespace modifies the namespace
   550  func ModifyNamespace(out io.Writer, dir string, env *v1.Environment, git gits.Gitter, chartMusemFn ResolveChartMuseumURLFn) error {
   551  	ns := env.Spec.Namespace
   552  	if ns == "" {
   553  		return fmt.Errorf("No Namespace is defined for Environment %s", env.Name)
   554  	}
   555  
   556  	// makefile changes
   557  	file := filepath.Join(dir, "Makefile")
   558  	exists, err := util.FileExists(file)
   559  	if err != nil {
   560  		return err
   561  	}
   562  	if !exists {
   563  		log.Logger().Warnf("WARNING: Could not find a Makefile in %s", dir)
   564  		return nil
   565  	}
   566  	input, err := ioutil.ReadFile(file)
   567  	if err != nil {
   568  		return err
   569  	}
   570  	lines := strings.Split(string(input), "\n")
   571  	err = ReplaceMakeVariable(lines, "NAMESPACE", "\""+ns+"\"")
   572  	if err != nil {
   573  		return err
   574  	}
   575  	output := strings.Join(lines, "\n")
   576  	err = ioutil.WriteFile(file, []byte(output), 0600)
   577  	if err != nil {
   578  		return err
   579  	}
   580  
   581  	// Jenkinsfile changes
   582  	file = filepath.Join(dir, "Jenkinsfile")
   583  	exists, err = util.FileExists(file)
   584  	if err != nil {
   585  		return err
   586  	}
   587  	if exists {
   588  		input, err := ioutil.ReadFile(file)
   589  		if err != nil {
   590  			return err
   591  		}
   592  		lines := strings.Split(string(input), "\n")
   593  		err = replaceEnvVar(lines, "DEPLOY_NAMESPACE", ns)
   594  		if err != nil {
   595  			return err
   596  		}
   597  		output := strings.Join(lines, "\n")
   598  		err = ioutil.WriteFile(file, []byte(output), 0600)
   599  		if err != nil {
   600  			return err
   601  		}
   602  	}
   603  
   604  	// lets ensure the namespace is set in a jenkins-x.yml file for tekton
   605  	projectConfig, projectConfigFile, err := config.LoadProjectConfig(dir)
   606  	if err != nil {
   607  		return err
   608  	}
   609  	foundEnv := false
   610  	for i := range projectConfig.Env {
   611  		if projectConfig.Env[i].Name == "DEPLOY_NAMESPACE" {
   612  			projectConfig.Env[i].Value = ns
   613  			foundEnv = true
   614  			break
   615  		}
   616  	}
   617  	if !foundEnv {
   618  		projectConfig.Env = append(projectConfig.Env, corev1.EnvVar{
   619  			Name:  "DEPLOY_NAMESPACE",
   620  			Value: ns,
   621  		})
   622  	}
   623  	foundEnv = false
   624  	pipelineConfig := projectConfig.PipelineConfig
   625  	if pipelineConfig == nil {
   626  		projectConfig.PipelineConfig = &jenkinsfile.PipelineConfig{}
   627  		pipelineConfig = projectConfig.PipelineConfig
   628  	}
   629  	for i := range pipelineConfig.Env {
   630  		if pipelineConfig.Env[i].Name == "DEPLOY_NAMESPACE" {
   631  			pipelineConfig.Env[i].Value = ns
   632  			foundEnv = true
   633  			break
   634  		}
   635  	}
   636  	if !foundEnv {
   637  		pipelineConfig.Env = append(pipelineConfig.Env, corev1.EnvVar{
   638  			Name:  "DEPLOY_NAMESPACE",
   639  			Value: ns,
   640  		})
   641  	}
   642  
   643  	if env.Spec.RemoteCluster && chartMusemFn != nil {
   644  		// lets ensure we have a chart museum env var
   645  		u, err := chartMusemFn()
   646  		if err != nil {
   647  			return errors.Wrapf(err, "failed to resolve Chart Museum URL for remote Environment %s", env.Name)
   648  		}
   649  		if u != "" {
   650  			pipelineConfig.Env = SetEnvVar(pipelineConfig.Env, "CHART_REPOSITORY", u)
   651  		}
   652  	}
   653  
   654  	err = projectConfig.SaveConfig(projectConfigFile)
   655  	if err != nil {
   656  		return err
   657  	}
   658  
   659  	err = git.Add(dir, "*")
   660  	if err != nil {
   661  		return err
   662  	}
   663  	changes, err := git.HasChanges(dir)
   664  	if err != nil {
   665  		return err
   666  	}
   667  	if changes {
   668  		return git.CommitDir(dir, "Use correct namespace for environment")
   669  	}
   670  	return nil
   671  }
   672  
   673  func addValues(out io.Writer, dir string, values config.HelmValuesConfig, git gits.Gitter) error {
   674  	file := filepath.Join(dir, "env", "values.yaml")
   675  	exists, err := util.FileExists(file)
   676  	if err != nil {
   677  		return err
   678  	}
   679  	if !exists {
   680  		return fmt.Errorf("could not find a values.yaml in %s", dir)
   681  	}
   682  
   683  	oldText, err := ioutil.ReadFile(file)
   684  	if err != nil {
   685  		return err
   686  	}
   687  
   688  	text, err := values.String()
   689  	if err != nil {
   690  		return err
   691  	}
   692  
   693  	sourceMap := map[string]interface{}{}
   694  	overrideMap := map[string]interface{}{}
   695  	err = yaml.Unmarshal(oldText, &sourceMap)
   696  	if err != nil {
   697  		return errors.Wrapf(err, "failed to parse YAML for file %s", file)
   698  	}
   699  	err = yaml.Unmarshal([]byte(text), &overrideMap)
   700  	if err != nil {
   701  		return errors.Wrapf(err, "failed to parse YAML for file %s", file)
   702  	}
   703  
   704  	if sourceMap != nil {
   705  		// now lets merge together the 2 blobs of YAML
   706  		util.CombineMapTrees(sourceMap, overrideMap)
   707  	} else {
   708  		sourceMap = overrideMap
   709  	}
   710  
   711  	output, err := yaml.Marshal(sourceMap)
   712  	if err != nil {
   713  		return errors.Wrap(err, "Failed to marshal the combined values YAML files back to YAML")
   714  	}
   715  	err = ioutil.WriteFile(file, output, util.DefaultWritePermissions)
   716  	if err != nil {
   717  		return errors.Wrapf(err, "Failed to save YAML file %s", file)
   718  	}
   719  
   720  	err = git.Add(dir, "*")
   721  	if err != nil {
   722  		return err
   723  	}
   724  	changes, err := git.HasChanges(dir)
   725  	if err != nil {
   726  		return err
   727  	}
   728  	if changes {
   729  		return git.CommitDir(dir, "Add environment configuration")
   730  	}
   731  	return nil
   732  }
   733  
   734  // ReplaceMakeVariable needs a description
   735  func ReplaceMakeVariable(lines []string, name string, value string) error {
   736  	re, err := regexp.Compile(name + "\\s*:?=\\s*(.*)")
   737  	if err != nil {
   738  		return err
   739  	}
   740  	replaceValue := name + " := " + value
   741  	for i, line := range lines {
   742  		lines[i] = re.ReplaceAllString(line, replaceValue)
   743  	}
   744  	return nil
   745  }
   746  
   747  func replaceEnvVar(lines []string, name string, value string) error {
   748  	for i, line := range lines {
   749  		trimmed := strings.TrimSpace(line)
   750  		if strings.HasPrefix(trimmed, name) {
   751  			remain := strings.TrimSpace(strings.TrimPrefix(trimmed, name))
   752  			if strings.HasPrefix(remain, "=") {
   753  				// lets preserve whitespace
   754  				idx := strings.Index(line, name)
   755  				lines[i] = line[0:idx] + name + ` = "` + value + `"`
   756  			}
   757  		}
   758  	}
   759  	return nil
   760  }
   761  
   762  // GetEnvironmentNames returns the sorted list of environment names
   763  func GetEnvironmentNames(jxClient versioned.Interface, ns string) ([]string, error) {
   764  	envNames := []string{}
   765  	envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   766  	if err != nil {
   767  		return envNames, err
   768  	}
   769  	SortEnvironments(envs.Items)
   770  	for _, env := range envs.Items {
   771  		n := env.Name
   772  		if n != "" {
   773  			envNames = append(envNames, n)
   774  		}
   775  	}
   776  	sort.Strings(envNames)
   777  	return envNames, nil
   778  }
   779  
   780  func IsPreviewEnvironment(env *v1.Environment) bool {
   781  	return env != nil && env.Spec.Kind == v1.EnvironmentKindTypePreview
   782  }
   783  
   784  // GetFilteredEnvironmentNames returns the sorted list of environment names
   785  func GetFilteredEnvironmentNames(jxClient versioned.Interface, ns string, fn func(environment *v1.Environment) bool) ([]string, error) {
   786  	envNames := []string{}
   787  	envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   788  	if err != nil {
   789  		return envNames, err
   790  	}
   791  	SortEnvironments(envs.Items)
   792  	for _, e := range envs.Items {
   793  		env := e
   794  		n := env.Name
   795  		if n != "" && fn(&env) {
   796  			envNames = append(envNames, n)
   797  		}
   798  	}
   799  	sort.Strings(envNames)
   800  	return envNames, nil
   801  }
   802  
   803  // GetOrderedEnvironments returns a map of the environments along with the correctly ordered  names
   804  func GetOrderedEnvironments(jxClient versioned.Interface, ns string) (map[string]*v1.Environment, []string, error) {
   805  	m := map[string]*v1.Environment{}
   806  
   807  	envNames := []string{}
   808  	envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   809  	if err != nil {
   810  		return m, envNames, err
   811  	}
   812  	SortEnvironments(envs.Items)
   813  	for _, env := range envs.Items {
   814  		n := env.Name
   815  		copy := env
   816  		m[n] = &copy
   817  		if n != "" {
   818  			envNames = append(envNames, n)
   819  		}
   820  	}
   821  	return m, envNames, nil
   822  }
   823  
   824  // GetEnvironments returns a map of the environments along with a sorted list of names
   825  func GetEnvironments(jxClient versioned.Interface, ns string) (map[string]*v1.Environment, []string, error) {
   826  	m := map[string]*v1.Environment{}
   827  
   828  	envNames := []string{}
   829  	envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   830  	if err != nil {
   831  		return m, envNames, err
   832  	}
   833  	for _, env := range envs.Items {
   834  		n := env.Name
   835  		copy := env
   836  		m[n] = &copy
   837  		if n != "" {
   838  			envNames = append(envNames, n)
   839  		}
   840  	}
   841  	sort.Strings(envNames)
   842  	return m, envNames, nil
   843  }
   844  
   845  // GetEnvironment find an environment by name
   846  func GetEnvironment(jxClient versioned.Interface, ns string, name string) (*v1.Environment, error) {
   847  	envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   848  	if err != nil {
   849  		return nil, err
   850  	}
   851  	for _, env := range envs.Items {
   852  		if env.GetName() == name {
   853  			return &env, nil
   854  		}
   855  	}
   856  	return nil, fmt.Errorf("no environment with name '%s' found", name)
   857  }
   858  
   859  // GetEnvironmentsByPrURL find an environment by a pull request URL
   860  func GetEnvironmentsByPrURL(jxClient versioned.Interface, ns string, prURL string) (*v1.Environment, error) {
   861  	envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   862  	if err != nil {
   863  		return nil, err
   864  	}
   865  	for _, env := range envs.Items {
   866  		if env.Spec.PullRequestURL == prURL {
   867  			return &env, nil
   868  		}
   869  	}
   870  	return nil, fmt.Errorf("no environment found for PR '%s'", prURL)
   871  }
   872  
   873  // GetEnvironments returns the namespace name for a given environment
   874  func GetEnvironmentNamespace(jxClient versioned.Interface, ns, environment string) (string, error) {
   875  	env, err := jxClient.JenkinsV1().Environments(ns).Get(environment, metav1.GetOptions{})
   876  	if err != nil {
   877  		return "", err
   878  	}
   879  	if env == nil {
   880  		return "", fmt.Errorf("no environment found called %s, try running `jx get env`", environment)
   881  	}
   882  	return env.Spec.Namespace, nil
   883  }
   884  
   885  // GetEditEnvironmentNamespace returns the namespace of the current users edit environment
   886  func GetEditEnvironmentNamespace(jxClient versioned.Interface, ns string) (string, error) {
   887  	envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   888  	if err != nil {
   889  		return "", err
   890  	}
   891  	u, err := user.Current()
   892  	if err != nil {
   893  		return "", err
   894  	}
   895  	for _, env := range envs.Items {
   896  		if env.Spec.Kind == v1.EnvironmentKindTypeEdit && env.Spec.PreviewGitSpec.User.Username == u.Username {
   897  			return env.Spec.Namespace, nil
   898  		}
   899  	}
   900  	return "", fmt.Errorf("The user %s does not have an Edit environment in home namespace %s", u.Username, ns)
   901  }
   902  
   903  // GetDevNamespace returns the developer environment namespace
   904  // which is the namespace that contains the Environments and the developer tools like Jenkins
   905  func GetDevNamespace(kubeClient kubernetes.Interface, ns string) (string, string, error) {
   906  	env := ""
   907  	namespace, err := kubeClient.CoreV1().Namespaces().Get(ns, metav1.GetOptions{})
   908  	if err != nil {
   909  		return ns, env, err
   910  	}
   911  	if namespace == nil {
   912  		return ns, env, fmt.Errorf("No namespace found for %s", ns)
   913  	}
   914  	if namespace.Labels != nil {
   915  		answer := namespace.Labels[LabelTeam]
   916  		if answer != "" {
   917  			ns = answer
   918  		}
   919  		env = namespace.Labels[LabelEnvironment]
   920  	}
   921  	return ns, env, nil
   922  }
   923  
   924  // GetTeams returns the Teams the user is a member of
   925  func GetTeams(kubeClient kubernetes.Interface) ([]*corev1.Namespace, []string, error) {
   926  	names := []string{}
   927  	answer := []*corev1.Namespace{}
   928  	namespaceList, err := kubeClient.CoreV1().Namespaces().List(metav1.ListOptions{})
   929  	if err != nil {
   930  		return answer, names, err
   931  	}
   932  	for idx, namespace := range namespaceList.Items {
   933  		if namespace.Labels[LabelEnvironment] == LabelValueDevEnvironment {
   934  			answer = append(answer, &namespaceList.Items[idx])
   935  			names = append(names, namespace.Name)
   936  		}
   937  	}
   938  	sort.Strings(names)
   939  	return answer, names, nil
   940  }
   941  
   942  func PickEnvironment(envNames []string, defaultEnv string, handles util.IOFileHandles) (string, error) {
   943  	surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err)
   944  	name := ""
   945  	if len(envNames) == 0 {
   946  		return "", nil
   947  	} else if len(envNames) == 1 {
   948  		name = envNames[0]
   949  	} else {
   950  		prompt := &survey.Select{
   951  			Message: "Pick environment:",
   952  			Options: envNames,
   953  			Default: defaultEnv,
   954  		}
   955  		err := survey.AskOne(prompt, &name, nil, surveyOpts)
   956  		if err != nil {
   957  			return "", err
   958  		}
   959  	}
   960  	return name, nil
   961  }
   962  
   963  type ByOrder []v1.Environment
   964  
   965  func (a ByOrder) Len() int      { return len(a) }
   966  func (a ByOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   967  func (a ByOrder) Less(i, j int) bool {
   968  	env1 := a[i]
   969  	env2 := a[j]
   970  	o1 := env1.Spec.Order
   971  	o2 := env2.Spec.Order
   972  	if o1 == o2 {
   973  		return env1.Name < env2.Name
   974  	}
   975  	return o1 < o2
   976  }
   977  
   978  func SortEnvironments(environments []v1.Environment) {
   979  	sort.Sort(ByOrder(environments))
   980  }
   981  
   982  // ByTimestamp is used to fileter a list of PipelineActivities by their given timestamp
   983  type ByTimestamp []v1.PipelineActivity
   984  
   985  func (a ByTimestamp) Len() int      { return len(a) }
   986  func (a ByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   987  func (a ByTimestamp) Less(i, j int) bool {
   988  	act1 := a[i]
   989  	act2 := a[j]
   990  	t1 := act1.Spec.StartedTimestamp
   991  	if t1 == nil {
   992  		return false
   993  	}
   994  	t2 := act2.Spec.StartedTimestamp
   995  	if t2 == nil {
   996  		return true
   997  	}
   998  
   999  	return t1.Before(t2)
  1000  }
  1001  
  1002  // SortActivities sorts a list of PipelineActivities
  1003  func SortActivities(activities []v1.PipelineActivity) {
  1004  	sort.Sort(ByTimestamp(activities))
  1005  }
  1006  
  1007  // NewPermanentEnvironment creates a new permanent environment for testing
  1008  func NewPermanentEnvironment(name string) *v1.Environment {
  1009  	return &v1.Environment{
  1010  		ObjectMeta: metav1.ObjectMeta{
  1011  			Name:      name,
  1012  			Namespace: "jx",
  1013  		},
  1014  		Spec: v1.EnvironmentSpec{
  1015  			Label:             strings.Title(name),
  1016  			Namespace:         "jx-" + name,
  1017  			PromotionStrategy: v1.PromotionStrategyTypeAutomatic,
  1018  			Kind:              v1.EnvironmentKindTypePermanent,
  1019  		},
  1020  	}
  1021  }
  1022  
  1023  // NewPermanentEnvironment creates a new permanent environment for testing
  1024  func NewPermanentEnvironmentWithGit(name string, gitUrl string) *v1.Environment {
  1025  	env := NewPermanentEnvironment(name)
  1026  	env.Spec.Source.URL = gitUrl
  1027  	env.Spec.Source.Ref = "master"
  1028  	return env
  1029  }
  1030  
  1031  // NewPreviewEnvironment creates a new preview environment for testing
  1032  func NewPreviewEnvironment(name string) *v1.Environment {
  1033  	return &v1.Environment{
  1034  		ObjectMeta: metav1.ObjectMeta{
  1035  			Name:      name,
  1036  			Namespace: "jx",
  1037  		},
  1038  		Spec: v1.EnvironmentSpec{
  1039  			Label:             strings.Title(name),
  1040  			Namespace:         "jx-preview-" + name,
  1041  			PromotionStrategy: v1.PromotionStrategyTypeAutomatic,
  1042  			Kind:              v1.EnvironmentKindTypePreview,
  1043  		},
  1044  	}
  1045  }
  1046  
  1047  // GetDevEnvironment returns the current development environment using the jxClient for the given ns.
  1048  // If the Dev Environment cannot be found, returns nil Environment (rather than an error). A non-nil error is only
  1049  // returned if there is an error fetching the Dev Environment.
  1050  func GetDevEnvironment(jxClient versioned.Interface, ns string) (*v1.Environment, error) {
  1051  	//Find the settings for the team
  1052  	environmentInterface := jxClient.JenkinsV1().Environments(ns)
  1053  	name := LabelValueDevEnvironment
  1054  	answer, err := environmentInterface.Get(name, metav1.GetOptions{})
  1055  	if err == nil {
  1056  		return answer, nil
  1057  	}
  1058  	selector := "env=dev"
  1059  	envList, err := environmentInterface.List(metav1.ListOptions{
  1060  		LabelSelector: selector,
  1061  	})
  1062  	if err != nil {
  1063  		return nil, err
  1064  	}
  1065  	if len(envList.Items) == 1 {
  1066  		return &envList.Items[0], nil
  1067  	}
  1068  	if len(envList.Items) == 0 {
  1069  		return nil, nil
  1070  	}
  1071  	return nil, fmt.Errorf("Error fetching dev environment resource definition in namespace %s, No Environment called: %s or with selector: %s found %d entries: %v",
  1072  		ns, name, selector, len(envList.Items), envList.Items)
  1073  }
  1074  
  1075  // GetPreviewEnvironmentReleaseName returns the (helm) release name for the given (preview) environment
  1076  // or the empty string is the environment is not a preview environment, or has no release name associated with it
  1077  func GetPreviewEnvironmentReleaseName(env *v1.Environment) string {
  1078  	if !IsPreviewEnvironment(env) {
  1079  		return ""
  1080  	}
  1081  	return env.Annotations[AnnotationReleaseName]
  1082  }
  1083  
  1084  // IsPermanentEnvironment indicates if an environment is permanent
  1085  func IsPermanentEnvironment(env *v1.Environment) bool {
  1086  	return env.Spec.Kind == v1.EnvironmentKindTypePermanent
  1087  }
  1088  
  1089  // GetPermanentEnvironments returns a list with the current permanent environments
  1090  func GetPermanentEnvironments(jxClient versioned.Interface, ns string) ([]*v1.Environment, error) {
  1091  	result := []*v1.Environment{}
  1092  	envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
  1093  	if err != nil {
  1094  		return result, errors.Wrapf(err, "listing the environments in namespace %q", ns)
  1095  	}
  1096  	for i := range envs.Items {
  1097  		env := &envs.Items[i]
  1098  		if IsPermanentEnvironment(env) {
  1099  			result = append(result, env)
  1100  		}
  1101  	}
  1102  	return result, nil
  1103  }