github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/step_split_monorepo.go (about)

     1  package step
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/olli-ai/jx/v2/pkg/cmd/opts/step"
    10  
    11  	"github.com/olli-ai/jx/v2/pkg/cmd/helper"
    12  
    13  	"github.com/jenkins-x/jx-logging/pkg/log"
    14  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    15  	"github.com/olli-ai/jx/v2/pkg/cmd/templates"
    16  	"github.com/olli-ai/jx/v2/pkg/gits"
    17  	"github.com/olli-ai/jx/v2/pkg/util"
    18  	"github.com/spf13/cobra"
    19  )
    20  
    21  const (
    22  	optionOrganisation   = "organisation"
    23  	defaultKubernetesDir = "kubernetes"
    24  )
    25  
    26  var (
    27  	stepSplitMonorepoLong = templates.LongDesc(`
    28  		Mirrors the code from a monorepo into separate microservice style Git repositories so its easier to do finer grained releases.
    29  
    30  		If you have lots of apps in folders in a monorepo then this command can run on that repo to mirror changes into a number of microservice based repositories which can each then get auto-imported into Jenkins X
    31  
    32  `)
    33  
    34  	stepSplitMonorepoExample = templates.Examples(`
    35  		# Split the current folder up into separate Git repositories 
    36  		jx step split monorepo -o mygithuborg
    37  			`)
    38  )
    39  
    40  // StepSplitMonorepoOptions contains the command line flags
    41  type StepSplitMonorepoOptions struct {
    42  	step.StepOptions
    43  
    44  	Glob          string
    45  	Organisation  string
    46  	RepoName      string
    47  	Dir           string
    48  	OutputDir     string
    49  	KubernetesDir string
    50  	NoGit         bool
    51  	PrivateGit    bool
    52  }
    53  
    54  // NewCmdStepSplitMonorepo Creates a new Command object
    55  func NewCmdStepSplitMonorepo(commonOpts *opts.CommonOptions) *cobra.Command {
    56  	options := &StepSplitMonorepoOptions{
    57  		StepOptions: step.StepOptions{
    58  			CommonOptions: commonOpts,
    59  		},
    60  	}
    61  
    62  	cmd := &cobra.Command{
    63  		Use:     "split monorepo",
    64  		Short:   "Mirrors the code from a monorepo into separate microservice style Git repositories so its easier to do finer grained releases",
    65  		Long:    stepSplitMonorepoLong,
    66  		Example: stepSplitMonorepoExample,
    67  		Run: func(cmd *cobra.Command, args []string) {
    68  			options.Cmd = cmd
    69  			options.Args = args
    70  			err := options.Run()
    71  			helper.CheckErr(err)
    72  		},
    73  	}
    74  	cmd.Flags().StringVarP(&options.Glob, "glob", "g", "*", "The glob pattern to find folders to mirror to separate repositories")
    75  	cmd.Flags().StringVarP(&options.Organisation, optionOrganisation, "o", "", "The GitHub organisation to split the repositories into")
    76  	cmd.Flags().StringVarP(&options.RepoName, "reponame", "n", "", "The GitHub monorepo to be split")
    77  	cmd.Flags().StringVarP(&options.Dir, "source-dir", "s", "", "The source directory to look inside for the folders to move into separate Git repositories")
    78  	cmd.Flags().StringVarP(&options.OutputDir, opts.OptionOutputDir, "d", "generated", "The output directory where new projects are created")
    79  	cmd.Flags().StringVarP(&options.KubernetesDir, "kubernetes-folder", "", defaultKubernetesDir, "The folder containing all the Kubernetes YAML for each app")
    80  	cmd.Flags().BoolVarP(&options.NoGit, "no-git", "", false, "If enabled then don't try to clone/create the separate repositories in github")
    81  	cmd.Flags().BoolVarP(&options.PrivateGit, "private-git", "", false, "If enabled then make clone/create to a private github repository")
    82  	return cmd
    83  }
    84  
    85  // Run implements this command
    86  func (o *StepSplitMonorepoOptions) Run() error {
    87  	organisation := o.Organisation
    88  	if organisation == "" {
    89  		return util.MissingOption(optionOrganisation)
    90  	}
    91  	reponame := o.RepoName
    92  	outputDir := o.OutputDir
    93  	if outputDir == "" {
    94  		return util.MissingOption(opts.OptionOutputDir)
    95  	}
    96  	var err error
    97  	dir := o.Dir
    98  	if dir == "" {
    99  		dir, err = os.Getwd()
   100  		if err != nil {
   101  			return err
   102  		}
   103  	}
   104  	glob := o.Glob
   105  
   106  	fullGlob := filepath.Join(dir, glob)
   107  	log.Logger().Debugf("Searching in monorepo at: %s", fullGlob)
   108  	matches, err := filepath.Glob(fullGlob)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	kubeDir := o.KubernetesDir
   113  	if kubeDir == "" {
   114  		kubeDir = defaultKubernetesDir
   115  	}
   116  	var gitProvider gits.GitProvider
   117  	if !o.NoGit {
   118  		gitProvider, err = o.GitProviderForGitServerURL(gits.GitHubURL, gits.KindGitHub, "")
   119  		if err != nil {
   120  			return err
   121  		}
   122  	}
   123  
   124  	for _, path := range matches {
   125  		_, name := filepath.Split(path)
   126  		if !strings.HasPrefix(name, ".") && name != kubeDir {
   127  			fi, err := os.Stat(path)
   128  			if err != nil {
   129  				return err
   130  			}
   131  			switch mode := fi.Mode(); {
   132  			case mode.IsDir():
   133  				log.Logger().Debugf("Found match: %s", path)
   134  				outPath := filepath.Join(outputDir, name)
   135  
   136  				var gitUrl string
   137  				var repo *gits.GitRepository
   138  				createRepo := true
   139  				if !o.NoGit {
   140  					// lets clone the project if it exists
   141  					repo, err = gitProvider.GetRepository(organisation, name)
   142  					if repo != nil && err == nil {
   143  						err = os.MkdirAll(outPath, util.DefaultWritePermissions)
   144  						if err != nil {
   145  							return err
   146  						}
   147  						createRepo = false
   148  						userAuth := gitProvider.UserAuth()
   149  						gitUrl, err = o.Git().CreateAuthenticatedURL(repo.CloneURL, &userAuth)
   150  						if err != nil {
   151  							return err
   152  						}
   153  						log.Logger().Infof("Cloning %s into directory %s", util.ColorInfo(repo.CloneURL), util.ColorInfo(outPath))
   154  						err = o.Git().CloneOrPull(gitUrl, outPath)
   155  						if err != nil {
   156  							return err
   157  						}
   158  					}
   159  				}
   160  
   161  				err = util.DeleteDirContentsExcept(outPath, ".git")
   162  				if err != nil {
   163  					return err
   164  				}
   165  
   166  				err = util.CopyDirOverwrite(path, outPath)
   167  				if err != nil {
   168  					return err
   169  				}
   170  
   171  				// lets copy the .gitignore
   172  				localGitIgnore := filepath.Join(outPath, ".gitignore")
   173  				exists, err := util.FileExists(localGitIgnore)
   174  				if err != nil {
   175  					return err
   176  				}
   177  				if !exists {
   178  					rootGitIgnore := filepath.Join(dir, ".gitignore")
   179  					exists, err = util.FileExists(rootGitIgnore)
   180  					if err != nil {
   181  						return err
   182  					}
   183  					if exists {
   184  						err = util.CopyFile(rootGitIgnore, localGitIgnore)
   185  						if err != nil {
   186  							return err
   187  						}
   188  					}
   189  				}
   190  
   191  				if !o.NoGit {
   192  					if createRepo {
   193  						repo, err = gitProvider.CreateRepository(organisation, name, o.PrivateGit)
   194  						if err != nil {
   195  							return err
   196  						}
   197  						log.Logger().Infof("Created Git repository to %s\n", util.ColorInfo(repo.HTMLURL))
   198  
   199  						userAuth := gitProvider.UserAuth()
   200  						gitUrl, err = o.Git().CreateAuthenticatedURL(repo.CloneURL, &userAuth)
   201  
   202  						err := o.Git().Init(outPath)
   203  						if err != nil {
   204  							return err
   205  						}
   206  						err = o.Git().AddRemote(outPath, "origin", gitUrl)
   207  						if err != nil {
   208  							return err
   209  						}
   210  					}
   211  					//ToDo: Why are we ignoring errors?
   212  					// ignore errors as probably already added
   213  					o.Git().Add(outPath, ".gitignore")         //nolint:errcheck
   214  					o.Git().Add(outPath, "src", "charts", "*") //nolint:errcheck
   215  
   216  					message := "generated by: jx step split monorepo"
   217  					if reponame != "" {
   218  						opt := &gits.ListCommitsArguments{
   219  							Path:    name,
   220  							Page:    1,
   221  							PerPage: 1,
   222  						}
   223  						commits, err := gitProvider.ListCommits(organisation, reponame, opt)
   224  						if err != nil {
   225  							return err
   226  						}
   227  						if len(commits) == 1 {
   228  							message = commits[0].Message + " - " + commits[0].SHA
   229  						}
   230  
   231  					}
   232  
   233  					err = o.Git().CommitIfChanges(outPath, message)
   234  					if err != nil {
   235  						return err
   236  					}
   237  					err = o.Git().PushMaster(outPath)
   238  					if err != nil {
   239  						return err
   240  					}
   241  					log.Logger().Infof("Pushed Git repository to %s\n", util.ColorInfo(repo.HTMLURL))
   242  				}
   243  			}
   244  		}
   245  	}
   246  	if kubeDir != "" {
   247  
   248  		// now lets copy any Kubernetes YAML into Helm charts in the apps
   249  		matches, err = filepath.Glob(filepath.Join(dir, kubeDir, "*"))
   250  		if err != nil {
   251  			return err
   252  		}
   253  		for _, path := range matches {
   254  			_, name := filepath.Split(path)
   255  			if strings.HasSuffix(name, ".yaml") {
   256  				appName := strings.TrimSuffix(name, ".yaml")
   257  				outPath := filepath.Join(outputDir, appName)
   258  				exists, err := util.DirExists(outPath)
   259  				if err != nil {
   260  					return err
   261  				}
   262  				if !exists && strings.HasSuffix(appName, "-deployment") {
   263  					// lets try strip "-deployment" from the file name
   264  					appName = strings.TrimSuffix(appName, "-deployment")
   265  					outPath = filepath.Join(outputDir, appName)
   266  					exists, err = util.DirExists(outPath)
   267  					if err != nil {
   268  						return err
   269  					}
   270  				}
   271  				if exists {
   272  					chartDir := filepath.Join(outPath, "charts", appName)
   273  					templatesDir := filepath.Join(chartDir, "templates")
   274  					err = os.MkdirAll(templatesDir, util.DefaultWritePermissions)
   275  					if err != nil {
   276  						return err
   277  					}
   278  
   279  					valuesYaml := `replicaCount: 1`
   280  					chartYaml := `apiVersion: v1
   281  description: A Helm chart for Kubernetes
   282  icon: https://raw.githubusercontent.com/jenkins-x/jenkins-x-platform/master/images/java.png
   283  name: ` + appName + `
   284  version: 0.0.1-SNAPSHOT
   285  `
   286  					helmIgnore := `# Patterns to ignore when building packages.
   287  # This supports shell glob matching, relative path matching, and
   288  # negation (prefixed with !). Only one pattern per line.
   289  .DS_Store
   290  # Common VCS dirs
   291  .git/
   292  .gitignore
   293  .bzr/
   294  .bzrignore
   295  .hg/
   296  .hgignore
   297  .svn/
   298  # Common backup files
   299  *.swp
   300  *.bak
   301  *.tmp
   302  *~
   303  # Various IDEs
   304  .project
   305  .idea/
   306  *.tmproj`
   307  
   308  					err = generateFileIfMissing(filepath.Join(chartDir, "values.yaml"), valuesYaml)
   309  					if err != nil {
   310  						return err
   311  					}
   312  					err = generateFileIfMissing(filepath.Join(chartDir, "Chart.yaml"), chartYaml)
   313  					if err != nil {
   314  						return err
   315  					}
   316  					err = generateFileIfMissing(filepath.Join(chartDir, ".helmignore"), helmIgnore)
   317  					if err != nil {
   318  						return err
   319  					}
   320  
   321  					yaml, err := ioutil.ReadFile(path)
   322  					if err != nil {
   323  						return err
   324  					}
   325  					err = generateFileIfMissing(filepath.Join(templatesDir, "deployment.yaml"), string(yaml))
   326  					if err != nil {
   327  						return err
   328  					}
   329  				}
   330  
   331  			}
   332  		}
   333  	}
   334  	return nil
   335  }
   336  
   337  // generateFileIfMissing generates the given file from the source code if the file does not already exist
   338  func generateFileIfMissing(path string, text string) error {
   339  	exists, err := util.FileExists(path)
   340  	if err != nil {
   341  		return err
   342  	}
   343  	if !exists {
   344  		return ioutil.WriteFile(path, []byte(text), util.DefaultWritePermissions)
   345  	}
   346  	return nil
   347  }