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  }