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 }