...

Source file src/github.com/redhat-developer/odo/pkg/init/init.go

Documentation: github.com/redhat-developer/odo/pkg/init

     1  package init
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/AlecAivazis/survey/v2/terminal"
    12  
    13  	"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
    14  	"github.com/devfile/library/v2/pkg/devfile/parser"
    15  	dfutil "github.com/devfile/library/v2/pkg/util"
    16  
    17  	"github.com/redhat-developer/odo/pkg/alizer"
    18  	"github.com/redhat-developer/odo/pkg/api"
    19  	"github.com/redhat-developer/odo/pkg/devfile"
    20  	"github.com/redhat-developer/odo/pkg/devfile/location"
    21  	"github.com/redhat-developer/odo/pkg/init/asker"
    22  	"github.com/redhat-developer/odo/pkg/init/backend"
    23  	"github.com/redhat-developer/odo/pkg/log"
    24  	"github.com/redhat-developer/odo/pkg/preference"
    25  	"github.com/redhat-developer/odo/pkg/registry"
    26  	"github.com/redhat-developer/odo/pkg/segment"
    27  	"github.com/redhat-developer/odo/pkg/testingutil/filesystem"
    28  )
    29  
    30  type InitClient struct {
    31  	// Backends
    32  	flagsBackend       *backend.FlagsBackend
    33  	interactiveBackend *backend.InteractiveBackend
    34  	alizerBackend      *backend.AlizerBackend
    35  
    36  	// Clients
    37  	fsys             filesystem.Filesystem
    38  	preferenceClient preference.Client
    39  	registryClient   registry.Client
    40  }
    41  
    42  var _ Client = (*InitClient)(nil)
    43  
    44  // Explicit list of tags useful to select the right backend
    45  var _initFlags = []string{
    46  	backend.FLAG_NAME,
    47  	backend.FLAG_DEVFILE,
    48  	backend.FLAG_DEVFILE_REGISTRY,
    49  	backend.FLAG_STARTER,
    50  	backend.FLAG_DEVFILE_PATH,
    51  	backend.FLAG_DEVFILE_VERSION,
    52  	backend.FLAG_RUN_PORT,
    53  	backend.FLAG_ARCHITECTURE,
    54  }
    55  
    56  func NewInitClient(fsys filesystem.Filesystem, preferenceClient preference.Client, registryClient registry.Client, alizerClient alizer.Client) *InitClient {
    57  	// We create the asker client and the backends here and not at the CLI level, as we want to hide these details to the CLI
    58  	askerClient := asker.NewSurveyAsker()
    59  	return &InitClient{
    60  		flagsBackend:       backend.NewFlagsBackend(registryClient),
    61  		interactiveBackend: backend.NewInteractiveBackend(askerClient, registryClient, alizerClient),
    62  		alizerBackend:      backend.NewAlizerBackend(askerClient, alizerClient),
    63  		fsys:               fsys,
    64  		preferenceClient:   preferenceClient,
    65  		registryClient:     registryClient,
    66  	}
    67  }
    68  
    69  // GetFlags gets the flag specific to init operation so that it can correctly decide on the backend to be used
    70  // It ignores all the flags except the ones specific to init operation, for e.g. verbosity flag
    71  func (o *InitClient) GetFlags(flags map[string]string) map[string]string {
    72  	initFlags := map[string]string{}
    73  outer:
    74  	for flag, value := range flags {
    75  		for _, f := range _initFlags {
    76  			if flag == f {
    77  				initFlags[flag] = value
    78  				continue outer
    79  			}
    80  		}
    81  	}
    82  	return initFlags
    83  }
    84  
    85  // Validate calls Validate method of the adequate backend
    86  func (o *InitClient) Validate(flags map[string]string, fs filesystem.Filesystem, dir string) error {
    87  	var backend backend.InitBackend
    88  	if len(flags) == 0 {
    89  		backend = o.interactiveBackend
    90  	} else {
    91  		backend = o.flagsBackend
    92  	}
    93  	return backend.Validate(flags, fs, dir)
    94  }
    95  
    96  // SelectDevfile calls SelectDevfile methods of the adequate backend
    97  func (o *InitClient) SelectDevfile(ctx context.Context, flags map[string]string, fs filesystem.Filesystem, dir string) (*api.DetectionResult, error) {
    98  	var backend backend.InitBackend
    99  
   100  	empty, err := location.DirIsEmpty(fs, dir)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	if empty && len(flags) == 0 {
   105  		backend = o.interactiveBackend
   106  	} else if len(flags) == 0 {
   107  		backend = o.alizerBackend
   108  	} else {
   109  		backend = o.flagsBackend
   110  	}
   111  	location, err := backend.SelectDevfile(ctx, flags, fs, dir)
   112  	if err != nil || location == nil {
   113  		if backend == o.alizerBackend {
   114  			// Fallback to the Interactive Mode if Alizer could not determine the Devfile.
   115  			if err != nil {
   116  				if errors.Is(err, context.Canceled) || errors.Is(err, terminal.InterruptErr) {
   117  					return nil, err
   118  				}
   119  				log.Warningf("Could not determine a Devfile based on the files in the current directory: %v", err)
   120  			}
   121  			return o.interactiveBackend.SelectDevfile(ctx, flags, fs, dir)
   122  		}
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  		return nil, errors.New("unable to determine the devfile location")
   127  	}
   128  
   129  	return location, err
   130  }
   131  
   132  func (o *InitClient) DownloadDevfile(ctx context.Context, devfileLocation *api.DetectionResult, destDir string) (string, error) {
   133  	destDevfile := filepath.Join(destDir, "devfile.yaml")
   134  	if devfileLocation.DevfilePath != "" {
   135  		return destDevfile, o.downloadDirect(devfileLocation.DevfilePath, destDevfile)
   136  	} else {
   137  		devfile := devfileLocation.Devfile
   138  		if devfileLocation.DevfileVersion != "" {
   139  			devfile = fmt.Sprintf("%s:%s", devfileLocation.Devfile, devfileLocation.DevfileVersion)
   140  		}
   141  		return destDevfile, o.downloadFromRegistry(ctx, devfileLocation.DevfileRegistry, devfile, destDir, devfileLocation.Architectures)
   142  	}
   143  }
   144  
   145  // downloadDirect downloads a devfile at the provided URL and saves it in dest
   146  func (o *InitClient) downloadDirect(URL string, dest string) error {
   147  	parsedURL, err := url.Parse(URL)
   148  	if err != nil {
   149  		return err
   150  	}
   151  	if strings.HasPrefix(parsedURL.Scheme, "http") {
   152  		downloadSpinner := log.Spinnerf("Downloading devfile from %q", URL)
   153  		defer downloadSpinner.End(false)
   154  		params := dfutil.HTTPRequestParams{
   155  			URL: URL,
   156  		}
   157  		devfileData, err := o.registryClient.DownloadFileInMemory(params)
   158  		if err != nil {
   159  			return err
   160  		}
   161  		err = o.fsys.WriteFile(dest, devfileData, 0644)
   162  		if err != nil {
   163  			return err
   164  		}
   165  		downloadSpinner.End(true)
   166  	} else {
   167  		downloadSpinner := log.Spinnerf("Copying devfile from %q", URL)
   168  		defer downloadSpinner.End(false)
   169  		content, err := o.fsys.ReadFile(URL)
   170  		if err != nil {
   171  			return err
   172  		}
   173  		info, err := o.fsys.Stat(URL)
   174  		if err != nil {
   175  			return err
   176  		}
   177  		err = o.fsys.WriteFile(dest, content, info.Mode().Perm())
   178  		if err != nil {
   179  			return err
   180  		}
   181  		downloadSpinner.End(true)
   182  	}
   183  
   184  	return nil
   185  }
   186  
   187  // downloadFromRegistry downloads a devfile from the provided registry and saves it in dest
   188  // If registryName is empty, will try to download the devfile from the list of registries in preferences
   189  // The architectures value indicates to download a Devfile compatible with all of these architectures
   190  func (o *InitClient) downloadFromRegistry(ctx context.Context, registryName string, devfile string, dest string, architectures []string) error {
   191  	// setting NewIndexSchema ensures that the Devfile library pulls registry based on the stack version
   192  	registryOptions := segment.GetRegistryOptions(ctx)
   193  	registryOptions.NewIndexSchema = true
   194  	if len(architectures) > 0 {
   195  		registryOptions.Filter.Architectures = architectures
   196  	}
   197  
   198  	var downloadSpinner *log.Status
   199  	var forceRegistry bool
   200  	if registryName == "" {
   201  		downloadSpinner = log.Spinnerf("Downloading devfile %q", devfile)
   202  		forceRegistry = false
   203  	} else {
   204  		downloadSpinner = log.Spinnerf("Downloading devfile %q from registry %q", devfile, registryName)
   205  		forceRegistry = true
   206  	}
   207  	defer downloadSpinner.End(false)
   208  
   209  	registries, err := o.registryClient.GetDevfileRegistries(registryName)
   210  	if err != nil {
   211  		return err
   212  	}
   213  	for _, reg := range registries {
   214  		if forceRegistry && reg.Name == registryName {
   215  			err := o.registryClient.PullStackFromRegistry(reg.URL, devfile, dest, registryOptions)
   216  			if err != nil {
   217  				return err
   218  			}
   219  			downloadSpinner.End(true)
   220  			return nil
   221  		} else if !forceRegistry {
   222  			err := o.registryClient.PullStackFromRegistry(reg.URL, devfile, dest, registryOptions)
   223  			if err != nil {
   224  				continue
   225  			}
   226  			downloadSpinner.End(true)
   227  			return nil
   228  		}
   229  	}
   230  
   231  	return fmt.Errorf("unable to find the registry with name %q", devfile)
   232  }
   233  
   234  // SelectStarterProject calls SelectStarterProject methods of the adequate backend
   235  func (o *InitClient) SelectStarterProject(devfile parser.DevfileObj, flags map[string]string, isEmptyDir bool) (*v1alpha2.StarterProject, error) {
   236  	var backend backend.InitBackend
   237  
   238  	if isEmptyDir && len(flags) == 0 {
   239  		backend = o.interactiveBackend
   240  	} else if len(flags) == 0 {
   241  		backend = o.alizerBackend
   242  	} else {
   243  		backend = o.flagsBackend
   244  	}
   245  	return backend.SelectStarterProject(devfile, flags)
   246  }
   247  
   248  func (o *InitClient) DownloadStarterProject(starter *v1alpha2.StarterProject, dest string) (containsDevfile bool, err error) {
   249  	downloadSpinner := log.Spinnerf("Downloading starter project %q", starter.Name)
   250  	containsDevfile, err = o.registryClient.DownloadStarterProject(starter, "", dest, false)
   251  	if err != nil {
   252  		downloadSpinner.End(false)
   253  		return containsDevfile, err
   254  	}
   255  	downloadSpinner.End(true)
   256  	return containsDevfile, nil
   257  }
   258  
   259  // PersonalizeName calls PersonalizeName methods of the adequate backend
   260  func (o *InitClient) PersonalizeName(devfile parser.DevfileObj, flags map[string]string) (string, error) {
   261  	var backend backend.InitBackend
   262  
   263  	if len(flags) == 0 {
   264  		backend = o.interactiveBackend
   265  	} else {
   266  		backend = o.flagsBackend
   267  	}
   268  	return backend.PersonalizeName(devfile, flags)
   269  }
   270  
   271  func (o *InitClient) HandleApplicationPorts(devfileobj parser.DevfileObj, ports []int, flags map[string]string, fs filesystem.Filesystem, dir string) (parser.DevfileObj, error) {
   272  	var backend backend.InitBackend
   273  	onlyDevfile, err := location.DirContainsOnlyDevfile(fs, dir)
   274  	if err != nil {
   275  		return parser.DevfileObj{}, err
   276  	}
   277  
   278  	// Interactive mode since no flags are provided
   279  	if len(flags) == 0 && !onlyDevfile {
   280  		// Other files present in the directory; hence alizer is run
   281  		backend = o.interactiveBackend
   282  	} else {
   283  		backend = o.flagsBackend
   284  	}
   285  	return backend.HandleApplicationPorts(devfileobj, ports, flags)
   286  }
   287  
   288  func (o *InitClient) PersonalizeDevfileConfig(devfileobj parser.DevfileObj, flags map[string]string, fs filesystem.Filesystem, dir string) (parser.DevfileObj, error) {
   289  	var backend backend.InitBackend
   290  
   291  	// Interactive mode since no flags are provided
   292  	if len(flags) == 0 {
   293  		backend = o.interactiveBackend
   294  	} else {
   295  		backend = o.flagsBackend
   296  	}
   297  	return backend.PersonalizeDevfileConfig(devfileobj)
   298  }
   299  
   300  func (o *InitClient) SelectAndPersonalizeDevfile(ctx context.Context, flags map[string]string, contextDir string) (parser.DevfileObj, string, *api.DetectionResult, error) {
   301  	devfileLocation, err := o.SelectDevfile(ctx, flags, o.fsys, contextDir)
   302  	if err != nil {
   303  		return parser.DevfileObj{}, "", nil, err
   304  	}
   305  
   306  	devfilePath, err := o.DownloadDevfile(ctx, devfileLocation, contextDir)
   307  	if err != nil {
   308  		return parser.DevfileObj{}, "", nil, fmt.Errorf("unable to download devfile: %w", err)
   309  	}
   310  
   311  	devfileObj, err := devfile.ParseAndValidateFromFile(devfilePath, "", false)
   312  	if err != nil {
   313  		return parser.DevfileObj{}, "", nil, fmt.Errorf("unable to parse devfile: %w", err)
   314  	}
   315  
   316  	devfileObj, err = o.HandleApplicationPorts(devfileObj, devfileLocation.ApplicationPorts, flags, o.fsys, contextDir)
   317  	if err != nil {
   318  		return parser.DevfileObj{}, "", nil, fmt.Errorf("unable to set application ports in devfile: %w", err)
   319  	}
   320  
   321  	devfileObj, err = o.PersonalizeDevfileConfig(devfileObj, flags, o.fsys, contextDir)
   322  	if err != nil {
   323  		return parser.DevfileObj{}, "", nil, fmt.Errorf("failed to configure devfile: %w", err)
   324  	}
   325  	return devfileObj, devfilePath, devfileLocation, nil
   326  }
   327  
   328  func (o *InitClient) InitDevfile(ctx context.Context, flags map[string]string, contextDir string,
   329  	preInitHandlerFunc func(interactiveMode bool), newDevfileHandlerFunc func(newDevfileObj parser.DevfileObj) error) error {
   330  
   331  	containsDevfile, err := location.DirectoryContainsDevfile(o.fsys, contextDir)
   332  	if err != nil {
   333  		return err
   334  	}
   335  	if containsDevfile {
   336  		return nil
   337  	}
   338  
   339  	if preInitHandlerFunc != nil {
   340  		preInitHandlerFunc(len(flags) == 0)
   341  	}
   342  
   343  	devfileObj, _, _, err := o.SelectAndPersonalizeDevfile(ctx, map[string]string{}, contextDir)
   344  	if err != nil {
   345  		return err
   346  	}
   347  
   348  	// Set the name in the devfile but do not write it yet.
   349  	name, err := o.PersonalizeName(devfileObj, map[string]string{})
   350  	if err != nil {
   351  		return fmt.Errorf("failed to update the devfile's name: %w", err)
   352  	}
   353  	metadata := devfileObj.Data.GetMetadata()
   354  	metadata.Name = name
   355  	devfileObj.Data.SetMetadata(metadata)
   356  
   357  	if newDevfileHandlerFunc != nil {
   358  		err = newDevfileHandlerFunc(devfileObj)
   359  	}
   360  
   361  	return err
   362  }
   363  

View as plain text