github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/create/create_mlquickstart.go (about) 1 package create 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "os" 9 "sort" 10 "strings" 11 12 "github.com/pkg/errors" 13 14 "github.com/olli-ai/jx/v2/pkg/cmd/importcmd" 15 "github.com/olli-ai/jx/v2/pkg/util" 16 17 "github.com/olli-ai/jx/v2/pkg/cmd/helper" 18 19 v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1" 20 "github.com/jenkins-x/jx-logging/pkg/log" 21 "github.com/olli-ai/jx/v2/pkg/gits" 22 "github.com/olli-ai/jx/v2/pkg/kube" 23 "github.com/olli-ai/jx/v2/pkg/quickstarts" 24 "github.com/spf13/cobra" 25 26 "github.com/olli-ai/jx/v2/pkg/auth" 27 "github.com/olli-ai/jx/v2/pkg/cmd/opts" 28 "github.com/olli-ai/jx/v2/pkg/cmd/templates" 29 ) 30 31 const ( 32 // JenkinsXMLQuickstartsOrganisation is the default organisation for machine-learning quickstarts 33 JenkinsXMLQuickstartsOrganisation = "machine-learning-quickstarts" 34 ) 35 36 var ( 37 // DefaultMLQuickstartLocation is the default organisation for machine-learning quickstarts 38 DefaultMLQuickstartLocation = v1.QuickStartLocation{ 39 GitURL: gits.GitHubURL, 40 GitKind: gits.KindGitHub, 41 Owner: JenkinsXMLQuickstartsOrganisation, 42 Includes: []string{"ML-*"}, 43 Excludes: []string{"WIP-*"}, 44 } 45 ) 46 47 var ( 48 createMLQuickstartLong = templates.LongDesc(` 49 Create a new machine learning project from a sample/starter (found in https://github.com/machine-learning-quickstarts) 50 51 This will create two new projects for you from the selected template. One for training and one for deploying a model as a service. 52 It will exclude any work-in-progress repos (containing the "WIP-" pattern) 53 54 For more documentation see: [https://jenkins-x.io/developing/create-mlquickstart/](https://jenkins-x.io/developing/create-mlquickstart/) 55 56 ` + helper.SeeAlsoText("jx create project")) 57 58 createMLQuickstartExample = templates.Examples(` 59 Create a new machine learning project from a sample/starter (found in https://github.com/machine-learning-quickstarts) 60 61 This will create a new machine learning project for you from the selected template. 62 It will exclude any work-in-progress repos (containing the "WIP-" pattern) 63 64 jx create mlquickstart 65 66 jx create mlquickstart -f pytorch 67 `) 68 ) 69 70 // CreateMLQuickstartOptions the options for the create quickstart command 71 type CreateMLQuickstartOptions struct { 72 CreateProjectOptions 73 74 GitHubOrganisations []string 75 Filter quickstarts.QuickstartFilter 76 GitProvider gits.GitProvider 77 GitHost string 78 IgnoreTeam bool 79 } 80 81 type projectset struct { 82 Repo string 83 Tail string 84 } 85 86 // NewCmdCreateMLQuickstart creates a command object for the "create" command 87 func NewCmdCreateMLQuickstart(commonOpts *opts.CommonOptions) *cobra.Command { 88 options := &CreateMLQuickstartOptions{ 89 CreateProjectOptions: CreateProjectOptions{ 90 ImportOptions: importcmd.ImportOptions{ 91 CommonOptions: commonOpts, 92 }, 93 }, 94 } 95 96 cmd := &cobra.Command{ 97 Use: "mlquickstart", 98 Short: "Create a new machine learning app from a set of quickstarts and import the generated code into Git and Jenkins for CI/CD", 99 Long: createMLQuickstartLong, 100 Example: createMLQuickstartExample, 101 Aliases: []string{"arch"}, 102 Run: func(cmd *cobra.Command, args []string) { 103 options.Cmd = cmd 104 options.Args = args 105 err := options.Run() 106 helper.CheckErr(err) 107 }, 108 } 109 options.addCreateAppFlags(cmd) 110 111 cmd.Flags().StringArrayVarP(&options.GitHubOrganisations, "organisations", "g", []string{}, "The GitHub organisations to query for quickstarts") 112 cmd.Flags().StringArrayVarP(&options.Filter.Tags, "tag", "t", []string{}, "The tags on the quickstarts to filter") 113 cmd.Flags().StringVarP(&options.Filter.Owner, "owner", "", "", "The owner to filter on") 114 cmd.Flags().StringVarP(&options.Filter.Language, "language", "l", "", "The language to filter on") 115 cmd.Flags().StringVarP(&options.Filter.Framework, "framework", "", "", "The framework to filter on") 116 cmd.Flags().StringVarP(&options.GitHost, "git-host", "", "", "The Git server host if not using GitHub when pushing created project") 117 cmd.Flags().StringVarP(&options.Filter.Text, "filter", "f", "", "The text filter") 118 cmd.Flags().StringVarP(&options.Filter.ProjectName, "project-name", "p", "", "The project name (for use with -b batch mode)") 119 return cmd 120 } 121 122 // Run implements the generic Create command 123 func (o *CreateMLQuickstartOptions) Run() error { 124 log.Logger().Debugf("Running CreateMLQuickstart...") 125 126 interactive := true 127 if o.BatchMode { 128 interactive = false 129 log.Logger().Debugf("In batch mode.") 130 } 131 132 authConfigSvc, err := o.GitAuthConfigService() 133 if err != nil { 134 return err 135 } 136 config := authConfigSvc.Config() 137 138 var locations []v1.QuickStartLocation 139 if !o.IgnoreTeam { 140 jxClient, ns, err := o.JXClientAndDevNamespace() 141 if err != nil { 142 return err 143 } 144 145 locations, err = kube.GetQuickstartLocations(jxClient, ns) 146 if err != nil { 147 return err 148 } 149 foundDefault := false 150 for _, location := range locations { 151 if isMLRepo(location) { 152 log.Logger().Debugf("Location: %s ", location) 153 } else { 154 // Protect generic quickstart repos from 155 } 156 if location.GitURL == gits.GitHubURL && location.Owner == JenkinsXMLQuickstartsOrganisation { 157 foundDefault = true 158 } 159 } 160 161 // Add the default MLQuickstarts repo if it is missing 162 if !foundDefault { 163 locations = append(locations, DefaultMLQuickstartLocation) 164 165 callback := func(env *v1.Environment) error { 166 env.Spec.TeamSettings.QuickstartLocations = locations 167 log.Logger().Infof("Adding the default ml quickstart repo %s", util.ColorInfo(util.UrlJoin(DefaultMLQuickstartLocation.GitURL, DefaultMLQuickstartLocation.Owner))) 168 return nil 169 } 170 err = o.ModifyDevEnvironment(callback) 171 if err != nil { 172 return errors.Wrap(err, "unable to modify dev environment settings") 173 } 174 } 175 } 176 177 // lets add any extra github organisations from the CLI if they are not already configured 178 for _, org := range o.GitHubOrganisations { 179 found := false 180 for _, loc := range locations { 181 if loc.GitURL == gits.GitHubURL && loc.Owner == org { 182 found = true 183 break 184 } 185 } 186 if !found { 187 locations = append(locations, v1.QuickStartLocation{ 188 GitURL: gits.GitHubURL, 189 GitKind: gits.KindGitHub, 190 Owner: org, 191 Includes: []string{"ML-*"}, 192 Excludes: []string{"WIP-*"}, 193 }) 194 } 195 } 196 197 gitMap := map[string]map[string]v1.QuickStartLocation{} 198 for _, loc := range locations { 199 m := gitMap[loc.GitURL] 200 if m == nil { 201 m = map[string]v1.QuickStartLocation{} 202 gitMap[loc.GitURL] = m 203 } 204 m[loc.Owner] = loc 205 206 } 207 208 var details *gits.CreateRepoData 209 210 if !o.BatchMode { 211 details, err = o.GetGitRepositoryDetails() 212 if err != nil { 213 return err 214 } 215 216 o.Filter.ProjectName = details.RepoName 217 } 218 219 o.Filter.AllowML = true 220 221 model, err := o.LoadQuickstartsFromMap(config, gitMap) 222 if err != nil { 223 return fmt.Errorf("failed to load quickstarts: %s", err) 224 } 225 var q *quickstarts.QuickstartForm 226 if o.BatchMode { 227 q, err = pickMLProject(model, &o.Filter, o.BatchMode) 228 } else { 229 q, err = model.CreateSurvey(&o.Filter, o.BatchMode, o.GetIOFileHandles()) 230 } 231 232 if err != nil { 233 return err 234 } 235 if q == nil { 236 return fmt.Errorf("no quickstart chosen") 237 } 238 239 dir := o.OutDir 240 if dir == "" { 241 dir, err = os.Getwd() 242 if err != nil { 243 return err 244 } 245 } 246 247 w := &CreateQuickstartOptions{} 248 w.CreateProjectOptions = o.CreateProjectOptions 249 w.CommonOptions = o.CommonOptions 250 w.ImportOptions = o.ImportOptions 251 w.GitHubOrganisations = o.GitHubOrganisations 252 w.Filter = o.Filter 253 w.Filter.Text = q.Quickstart.Name 254 w.GitProvider = o.GitProvider 255 w.GitHost = o.GitHost 256 w.IgnoreTeam = o.IgnoreTeam 257 258 w.BatchMode = true 259 260 // Check to see if the selection is a project set 261 ps, err := o.getMLProjectSet(q.Quickstart) 262 263 var e error 264 if err == nil { 265 // We have a projectset so create all the associated quickstarts 266 stub := o.Filter.ProjectName 267 for _, project := range ps { 268 w.ImportOptions = o.ImportOptions // Reset the options each time as they are modified by Import (DraftPack) 269 if interactive { 270 log.Logger().Debugf("Setting Quickstart from surveys.") 271 w.ImportOptions.Organisation = details.Organisation 272 w.GitRepositoryOptions = o.GitRepositoryOptions 273 w.GitRepositoryOptions.ServerURL = details.GitServer.URL 274 w.GitRepositoryOptions.ServerKind = details.GitServer.Kind 275 w.GitRepositoryOptions.Username = details.User.Username 276 w.GitRepositoryOptions.ApiToken = details.User.ApiToken 277 w.GitRepositoryOptions.Owner = details.Organisation 278 w.GitRepositoryOptions.Public = details.Public 279 w.GitProvider = details.GitProvider 280 w.GitServer = details.GitServer 281 } 282 w.Filter.Text = project.Repo 283 w.Filter.ProjectName = stub + project.Tail 284 w.Filter.Language = "" 285 log.Logger().Debugf("Invoking CreateQuickstart for %s...", project.Repo) 286 287 e = w.Run() 288 289 if e != nil { 290 return e 291 } 292 } 293 } else { 294 // Must be a conventional quickstart 295 log.Logger().Debugf("Invoking CreateQuickstart...") 296 return w.Run() 297 } 298 299 return e 300 301 } 302 303 // Pairs of Training and Service projects can be declared by creating a dedicated repository that shares the same root name as the -Training and -Service repositories 304 // but which contains only a 'projectset' file that specifies the names of the associated projects. 305 // Selecting the projectset project as a quickstart automatically creates both related -Training and -Service projects with a common name prefix. 306 func (o *CreateMLQuickstartOptions) getMLProjectSet(q *quickstarts.Quickstart) ([]projectset, error) { 307 var ps []projectset 308 309 // Look at https://raw.githubusercontent.com/:owner/:repo/master/projectset 310 client := http.Client{} 311 u := "https://raw.githubusercontent.com/" + q.Owner + "/" + q.Name + "/master/projectset" 312 313 req, err := http.NewRequest(http.MethodGet, u, strings.NewReader("")) 314 if err != nil { 315 log.Logger().Debugf("Projectset not found because %+#v", err) 316 return nil, err 317 } 318 gitProvider := q.GitProvider 319 if gitProvider != nil { 320 userAuth := gitProvider.UserAuth() 321 token := userAuth.ApiToken 322 username := userAuth.Username 323 if token != "" && username != "" { 324 log.Logger().Debugf("Downloading project zip from %s with basic auth for user: %s", u, username) 325 req.SetBasicAuth(username, token) 326 } 327 } 328 res, err := client.Do(req) 329 if err != nil { 330 return nil, err 331 } 332 body, err := ioutil.ReadAll(res.Body) 333 if err != nil { 334 return nil, err 335 } 336 err = json.Unmarshal(body, &ps) 337 return ps, err 338 } 339 340 // LoadQuickstartsFromMap Load all quickstarts 341 func (o *CreateMLQuickstartOptions) LoadQuickstartsFromMap(config *auth.AuthConfig, gitMap map[string]map[string]v1.QuickStartLocation) (*quickstarts.QuickstartModel, error) { 342 model := quickstarts.NewQuickstartModel() 343 344 mlOnly := []string{"ML-*"} // Filter for ML repos 345 346 for gitURL, m := range gitMap { 347 for _, location := range m { 348 kind := location.GitKind 349 if kind == "" { 350 kind = gits.KindGitHub 351 } 352 gitProvider, err := o.GitProviderForGitServerURL(gitURL, kind, "") 353 if err != nil { 354 return model, err 355 } 356 log.Logger().Debugf("Searching for repositories in Git server %s owner %s includes %s excludes %s as user %s ", gitProvider.ServerURL(), location.Owner, strings.Join(mlOnly, ", "), strings.Join(location.Excludes, ", "), gitProvider.CurrentUsername()) 357 err = model.LoadGithubQuickstarts(gitProvider, location.Owner, mlOnly, location.Excludes) 358 if err != nil { 359 log.Logger().Debugf("Quickstart load error: %s", err.Error()) 360 } 361 } 362 } 363 364 return model, nil 365 } 366 367 // PickMLProject picks a mlquickstart project set from filtered results 368 func pickMLProject(model *quickstarts.QuickstartModel, filter *quickstarts.QuickstartFilter, batchMode bool) (*quickstarts.QuickstartForm, error) { 369 mlquickstarts := model.Filter(filter) 370 names := []string{} 371 m := map[string]*quickstarts.Quickstart{} 372 for _, qs := range mlquickstarts { 373 name := qs.SurveyName() 374 m[name] = qs 375 names = append(names, name) 376 } 377 sort.Strings(names) 378 379 if len(names) == 0 { 380 return nil, fmt.Errorf("No quickstarts match filter") 381 } 382 answer := "" 383 // Pick the first option as this is the project set 384 answer = names[0] 385 if answer == "" { 386 return nil, fmt.Errorf("No quickstart chosen") 387 } 388 q := m[answer] 389 if q == nil { 390 return nil, fmt.Errorf("Could not find chosen quickstart for %s", answer) 391 } 392 form := &quickstarts.QuickstartForm{ 393 Quickstart: q, 394 Name: q.Name, 395 } 396 return form, nil 397 } 398 399 // isMLRepo returns true if the git location has "ML-*" defined within Includes: 400 func isMLRepo(location v1.QuickStartLocation) bool { 401 for _, v := range location.Includes { 402 if v == "ML-*" { 403 return true 404 } 405 } 406 return false 407 }